From 741b17ee0a76c0d2c21910690e4ae2c3a76a241c Mon Sep 17 00:00:00 2001 From: Nelson Parente Date: Tue, 19 May 2026 22:07:53 +0100 Subject: [PATCH 1/2] feat(dotnet): add workflow context propagation quickstart sample Signed-off-by: Nelson Parente --- .../csharp/sdk-context-propagation/README.md | 155 +++++++++++ .../csharp/sdk-context-propagation/dapr.yaml | 7 + .../order-processor/Activities.cs | 71 +++++ .../order-processor/Models.cs | 49 ++++ .../order-processor/OrderProcessor.csproj | 17 ++ .../order-processor/Program.cs | 120 +++++++++ .../order-processor/Workflows.cs | 250 ++++++++++++++++++ 7 files changed, 669 insertions(+) create mode 100644 workflows/csharp/sdk-context-propagation/README.md create mode 100644 workflows/csharp/sdk-context-propagation/dapr.yaml create mode 100644 workflows/csharp/sdk-context-propagation/order-processor/Activities.cs create mode 100644 workflows/csharp/sdk-context-propagation/order-processor/Models.cs create mode 100644 workflows/csharp/sdk-context-propagation/order-processor/OrderProcessor.csproj create mode 100644 workflows/csharp/sdk-context-propagation/order-processor/Program.cs create mode 100644 workflows/csharp/sdk-context-propagation/order-processor/Workflows.cs diff --git a/workflows/csharp/sdk-context-propagation/README.md b/workflows/csharp/sdk-context-propagation/README.md new file mode 100644 index 000000000..43826a3d0 --- /dev/null +++ b/workflows/csharp/sdk-context-propagation/README.md @@ -0,0 +1,155 @@ +# Dapr Workflow — Context Propagation (.NET SDK) + +This quickstart demonstrates **workflow history propagation**, a new feature in Dapr 1.18 that lets a parent workflow share its execution history with child workflows. Downstream services can inspect that history to make trust-aware decisions — without any external state store or custom messaging. + +> **Runtime requirement**: Dapr 1.18+ ([dapr/dapr#9810](https://github.com/dapr/dapr/pull/9810)) +> **SDK requirement**: `Dapr.Workflow >= 1.18.0-rc01` ([dapr/dotnet-sdk#1802](https://github.com/dapr/dotnet-sdk/pull/1802)) +> **Proposal**: [dapr/proposals#102](https://github.com/dapr/proposals/issues/102) + +## What is workflow context propagation? + +When a parent workflow calls a child workflow it can optionally attach a tamper-evident snapshot of its own execution history. The receiver reads that snapshot via `ctx.GetPropagatedHistory()` and inspects the returned `PropagatedHistory` entries — letting it verify that the correct upstream steps ran before it proceeds. + +### Two propagation modes + +| Mode | Enum value | What the receiver sees | +|------|-----------|----------------------| +| **Own history** | `HistoryPropagationScope.OwnHistory` | Only the direct caller's events | +| **Lineage** | `HistoryPropagationScope.Lineage` | Caller's events **plus** any ancestor history the caller itself received | + +## Scenario: Credit-card payment with fraud detection + +``` +MerchantCheckout (root) + └─ ValidateMerchant (activity, no propagation) + └─ ProcessPayment (child wf, Lineage) + └─ ValidateCard (activity, no propagation) + └─ CheckSpendingLimits (activity, no propagation) + └─ FraudDetection (grandchild wf, Lineage) + | reads MerchantCheckout/ValidateMerchant + | ProcessPayment/ValidateCard + | ProcessPayment/CheckSpendingLimits + └─ SettlementWorkflow (grandchild wf, OwnHistory) + reads ProcessPayment events only + └─ SettlePayment (activity) +``` + +`FraudDetection` uses `HistoryPropagationScope.Lineage` to see the **full ancestor chain** — it can verify both the merchant validation (performed by the grandparent) and the card/limit checks (performed by the parent) before approving the transaction. + +`SettlementWorkflow` uses `HistoryPropagationScope.OwnHistory` to see only the **direct caller's events** — a trust-boundary mode that limits visibility to what `ProcessPayment` itself executed. + +### .NET vs Python difference + +The Python sibling ([dapr/quickstarts#1309](https://github.com/dapr/quickstarts/pull/1309)) calls `settle_payment` as a bare activity with `propagation=PropagationScope.OWN_HISTORY`. In the .NET SDK (v1.18) `HistoryPropagationScope` is only available on `ChildWorkflowTaskOptions` — activity calls do not carry a propagation scope. To demonstrate the identical trust-boundary semantics, this sample wraps the settlement activity inside `SettlementWorkflow` (a child workflow). + +## .NET API surface + +```csharp +// Parent workflow — propagate Lineage when calling a child workflow +var result = await ctx.CallChildWorkflowAsync( + nameof(FraudDetectionWorkflow), + input, + new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); + +// Parent workflow — propagate OwnHistory when calling a child workflow +var settlement = await ctx.CallChildWorkflowAsync( + nameof(SettlementWorkflow), + input, + new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); + +// Child workflow — read the propagated history +var history = ctx.GetPropagatedHistory(); // returns PropagatedHistory? + +if (history is not null) +{ + // Filter to a specific ancestor workflow by name + var processEntries = history.FilterByWorkflowName(nameof(ProcessPaymentWorkflow)); + + // Inspect events within that ancestor's segment + var completedCount = processEntries.Entries[0].Events + .Count(e => e.Kind == HistoryEventKind.TaskCompleted); +} +``` + +Key types in `Dapr.Workflow`: +- `HistoryPropagationScope` — enum: `None`, `OwnHistory`, `Lineage` +- `ChildWorkflowTaskOptions` — pass `PropagationScope` here +- `PropagatedHistory` — call `.FilterByWorkflowName(name)`, `.FilterByAppId(id)`, `.FilterByInstanceId(id)` +- `PropagatedHistoryEntry` — has `WorkflowName`, `AppId`, `InstanceId`, `Events` +- `PropagatedHistoryEvent` — has `EventId`, `Kind` (`HistoryEventKind`), `Timestamp` +- `HistoryEventKind` — enum including `TaskScheduled`, `TaskCompleted`, `TaskFailed`, etc. + +## Prerequisites + +- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) 1.18+ +- Dapr runtime 1.18+ initialized (`dapr init`) +- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) +- Redis (started automatically by `dapr init`) + +## Run the sample + +```sh +cd workflows/csharp/sdk-context-propagation + +dapr run -f . +``` + +## Expected output + +``` +============================================ += WORKFLOW HISTORY PROPAGATION DEMO (.NET) = +============================================ + + Flow: MerchantCheckout -> ValidateMerchant + -> ProcessPayment (child wf, Lineage) + -> ValidateCard -> CheckSpendingLimits + -> FraudDetection (child wf, Lineage) <-- sees MerchantCheckout + ProcessPayment events + -> SettlementWorkflow (child wf, OwnHistory) <-- sees only ProcessPayment events + + [main] Scheduling workflow instance: checkout-001 + [MerchantCheckout] Starting checkout for merchant merchant-abc + [MerchantCheckout] Step 1: ValidateMerchant (no propagation) + [ValidateMerchant] Validating merchant merchant-abc + [MerchantCheckout] Step 1 complete: merchant valid + [MerchantCheckout] Step 2: ProcessPayment child wf (HistoryPropagationScope.Lineage) + [ProcessPayment] Starting payment ****4242 149.99 USD + [ProcessPayment] Step 1: ValidateCard (no propagation) + [ValidateCard] Validating card ****4242 + [ProcessPayment] Step 1 complete: card valid + [ProcessPayment] Step 2: CheckSpendingLimits (no propagation) + [CheckSpendingLimits] Checking 149.99 USD + [CheckSpendingLimits] Within limits: True + [ProcessPayment] Step 2 complete: within limits + [ProcessPayment] Step 3: FraudDetection child wf (HistoryPropagationScope.Lineage) + [FraudDetection] Checking payment ****4242 149.99 USD + [FraudDetection] Received propagated history with 2 segment(s): + [FraudDetection] workflow: name=MerchantCheckoutWorkflow app=order-processor events=... + [FraudDetection] workflow: name=ProcessPaymentWorkflow app=order-processor events=... + [FraudDetection] Verification: + MerchantCheckout TaskCompleted events: 1 + ProcessPayment TaskCompleted events: 2 + [FraudDetection] APPROVED (risk=0.10, total events inspected=...) + [ProcessPayment] Step 3 complete: fraud check passed (risk=0.10) + [ProcessPayment] Step 4: SettlementWorkflow child wf (HistoryPropagationScope.OwnHistory) + [SettlementWorkflow] Propagated segments: 1 + [SettlementWorkflow] workflow: name=ProcessPaymentWorkflow app=order-processor events=... + [SettlementWorkflow] MerchantCheckout in history (expected 0): 0 + [SettlePayment] SETTLED: txn-merchant-abc-... + [MerchantCheckout] COMPLETE: payment settled: txn=txn-merchant-abc-..., card=****4242, amount=149.99 USD + [main] Workflow completed! Output: "payment settled: ..." + +============================================ += COMPLETE = +============================================ +``` + +## References + +- Sibling Python quickstart: [dapr/quickstarts#1309](https://github.com/dapr/quickstarts/pull/1309) +- Canonical Go SDK reference: [dapr/go-sdk#823](https://github.com/dapr/go-sdk/pull/823) +- .NET SDK implementation: [dapr/dotnet-sdk#1802](https://github.com/dapr/dotnet-sdk/pull/1802) +- Runtime support: [dapr/dapr#9810](https://github.com/dapr/dapr/pull/9810) +- Docs (.NET): [dapr/docs#5174](https://github.com/dapr/docs/pull/5174) +- Proposal: [dapr/proposals#102](https://github.com/dapr/proposals/issues/102) +- 1.18 endgame: [dapr/dapr#9856](https://github.com/dapr/dapr/issues/9856) diff --git a/workflows/csharp/sdk-context-propagation/dapr.yaml b/workflows/csharp/sdk-context-propagation/dapr.yaml new file mode 100644 index 000000000..a97cac6e5 --- /dev/null +++ b/workflows/csharp/sdk-context-propagation/dapr.yaml @@ -0,0 +1,7 @@ +version: 1 +common: + resourcesPath: ../../components +apps: + - appID: order-processor + appDirPath: ./order-processor/ + command: ["dotnet", "run"] diff --git a/workflows/csharp/sdk-context-propagation/order-processor/Activities.cs b/workflows/csharp/sdk-context-propagation/order-processor/Activities.cs new file mode 100644 index 000000000..7e5bdf167 --- /dev/null +++ b/workflows/csharp/sdk-context-propagation/order-processor/Activities.cs @@ -0,0 +1,71 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace OrderProcessor; + +using Dapr.Workflow; + +/// +/// Validates the merchant account. Called by MerchantCheckout without propagation. +/// +public sealed class ValidateMerchantActivity : WorkflowActivity +{ + public override Task RunAsync(WorkflowActivityContext ctx, PaymentRequest req) + { + Console.WriteLine($" [ValidateMerchant] Validating merchant {req.MerchantId}"); + return Task.FromResult(true); + } +} + +/// +/// Validates the payment card. Called by ProcessPayment without propagation. +/// +public sealed class ValidateCardActivity : WorkflowActivity +{ + public override Task RunAsync(WorkflowActivityContext ctx, PaymentRequest req) + { + Console.WriteLine($" [ValidateCard] Validating card ****{req.CardLast4}"); + return Task.FromResult(true); + } +} + +/// +/// Checks that the payment amount is within card spending limits. +/// Called by ProcessPayment without propagation. +/// +public sealed class CheckSpendingLimitsActivity : WorkflowActivity +{ + public override Task RunAsync(WorkflowActivityContext ctx, PaymentRequest req) + { + Console.WriteLine($" [CheckSpendingLimits] Checking {req.Amount} {req.Currency}"); + bool withinLimits = req.Amount <= 10_000; + Console.WriteLine($" [CheckSpendingLimits] Within limits: {withinLimits}"); + return Task.FromResult(withinLimits); + } +} + +/// +/// Executes the final payment settlement. Called by SettlementWorkflow. +/// +public sealed class SettlePaymentActivity : WorkflowActivity +{ + public override Task RunAsync(WorkflowActivityContext ctx, PaymentRequest req) + { + var txnId = $"txn-{req.MerchantId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; + Console.WriteLine($" [SettlePayment] SETTLED: {txnId}"); + return Task.FromResult(new SettlementResult( + TransactionId: txnId, + Status: "settled", + EventCount: 0)); // EventCount populated by SettlementWorkflow + } +} diff --git a/workflows/csharp/sdk-context-propagation/order-processor/Models.cs b/workflows/csharp/sdk-context-propagation/order-processor/Models.cs new file mode 100644 index 000000000..8c54f5e34 --- /dev/null +++ b/workflows/csharp/sdk-context-propagation/order-processor/Models.cs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace OrderProcessor; + +/// +/// Payment request passed through the workflow hierarchy. +/// +/// Last four digits of the payment card. +/// Amount to charge. +/// ISO 4217 currency code. +/// Merchant identifier. +/// Human-readable payment description. +public sealed record PaymentRequest( + string CardLast4, + double Amount, + string Currency, + string MerchantId, + string Description); + +/// Result produced by the FraudDetection workflow. +/// Risk score in the range [0, 1]. +/// Whether the transaction was approved. +/// Human-readable decision rationale. +/// Number of propagated history events inspected. +public sealed record FraudCheckResult( + double RiskScore, + bool Approved, + string Reason, + int EventCount); + +/// Result produced by the SettlePayment activity. +/// Unique transaction reference. +/// Settlement status string. +/// Number of propagated history events inspected. +public sealed record SettlementResult( + string TransactionId, + string Status, + int EventCount); diff --git a/workflows/csharp/sdk-context-propagation/order-processor/OrderProcessor.csproj b/workflows/csharp/sdk-context-propagation/order-processor/OrderProcessor.csproj new file mode 100644 index 000000000..6c373742c --- /dev/null +++ b/workflows/csharp/sdk-context-propagation/order-processor/OrderProcessor.csproj @@ -0,0 +1,17 @@ + + + + Exe + enable + net8.0 + enable + latest + + + + + + + + + diff --git a/workflows/csharp/sdk-context-propagation/order-processor/Program.cs b/workflows/csharp/sdk-context-propagation/order-processor/Program.cs new file mode 100644 index 000000000..6c98e0e0b --- /dev/null +++ b/workflows/csharp/sdk-context-propagation/order-processor/Program.cs @@ -0,0 +1,120 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +// Workflow History Propagation Quickstart (.NET SDK) +// +// Scenario: credit-card payment processing with fraud detection. +// +// Flow: +// MerchantCheckout (root) +// └─ ValidateMerchant (activity, no propagation) +// └─ ProcessPayment (child wf, HistoryPropagationScope.Lineage) +// └─ ValidateCard (activity, no propagation) +// └─ CheckSpendingLimits (activity, no propagation) +// └─ FraudDetection (grandchild wf, HistoryPropagationScope.Lineage) +// | reads: MerchantCheckout + ProcessPayment events +// └─ SettlePayment (activity, HistoryPropagationScope.OwnHistory) +// reads: ProcessPayment events only +// +// Requires Dapr 1.18+ (dapr/dapr#9810) and Dapr.Workflow 1.18+ (dapr/dotnet-sdk#1802). +// Against an older sidecar GetPropagatedHistory() returns null and the sample +// exits gracefully. + +using Dapr.Client; +using Dapr.Workflow; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OrderProcessor; + +const string Banner = + "============================================\n" + + "= WORKFLOW HISTORY PROPAGATION DEMO (.NET) =\n" + + "============================================"; + +// --------------------------------------------------------------------------- +// Host setup — register workflows and activities +// --------------------------------------------------------------------------- + +var builder = Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddDaprClient(); + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + options.RegisterWorkflow(); + options.RegisterWorkflow(); + options.RegisterWorkflow(); + + options.RegisterActivity(); + options.RegisterActivity(); + options.RegisterActivity(); + options.RegisterActivity(); + }); + }); + +using var host = builder.Build(); +host.Start(); + +var workflowClient = host.Services.GetRequiredService(); + +// --------------------------------------------------------------------------- +// Kick off the root workflow +// --------------------------------------------------------------------------- + +Console.WriteLine(Banner); +Console.WriteLine(); +Console.WriteLine(" Flow: MerchantCheckout -> ValidateMerchant"); +Console.WriteLine(" -> ProcessPayment (child wf, Lineage)"); +Console.WriteLine(" -> ValidateCard -> CheckSpendingLimits"); +Console.WriteLine(" -> FraudDetection (child wf, Lineage) <-- sees MerchantCheckout + ProcessPayment events"); +Console.WriteLine(" -> SettlePayment (activity, OwnHistory) <-- sees only ProcessPayment events"); +Console.WriteLine(); + +var request = new PaymentRequest( + CardLast4: "4242", + Amount: 149.99, + Currency: "USD", + MerchantId: "merchant-abc", + Description: "Online purchase"); + +const string InstanceId = "checkout-001"; + +Console.WriteLine($" [main] Scheduling workflow instance: {InstanceId}"); + +await workflowClient.ScheduleNewWorkflowAsync( + name: nameof(MerchantCheckoutWorkflow), + instanceId: InstanceId, + input: request); + +var state = await workflowClient.WaitForWorkflowCompletionAsync( + instanceId: InstanceId, + cancellationToken: new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token); + +if (state is null) +{ + Console.WriteLine(" [main] Workflow not found!"); +} +else if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) +{ + Console.WriteLine($" [main] Workflow completed! Output: {state.SerializedOutput}"); +} +else +{ + Console.WriteLine($" [main] Workflow ended with status: {state.RuntimeStatus}"); +} + +Console.WriteLine(); +Console.WriteLine("============================================"); +Console.WriteLine("= COMPLETE ="); +Console.WriteLine("============================================"); diff --git a/workflows/csharp/sdk-context-propagation/order-processor/Workflows.cs b/workflows/csharp/sdk-context-propagation/order-processor/Workflows.cs new file mode 100644 index 000000000..d2f0dd765 --- /dev/null +++ b/workflows/csharp/sdk-context-propagation/order-processor/Workflows.cs @@ -0,0 +1,250 @@ +// ------------------------------------------------------------------------ +// Copyright 2026 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace OrderProcessor; + +using Dapr.Workflow; + +// --------------------------------------------------------------------------- +// MerchantCheckout (root workflow) +// --------------------------------------------------------------------------- + +/// +/// Root workflow — validates the merchant then delegates payment to a child +/// workflow with full propagation +/// so the grandchild FraudDetection can inspect the complete ancestor chain. +/// +public sealed class MerchantCheckoutWorkflow : Workflow +{ + public override async Task RunAsync(WorkflowContext ctx, PaymentRequest req) + { + Console.WriteLine($" [MerchantCheckout] Starting checkout for merchant {req.MerchantId}"); + + // Step 1: Validate merchant — no propagation (plain activity). + Console.WriteLine(" [MerchantCheckout] Step 1: ValidateMerchant (no propagation)"); + await ctx.CallActivityAsync( + nameof(ValidateMerchantActivity), + req); + Console.WriteLine(" [MerchantCheckout] Step 1 complete: merchant valid"); + + // Step 2: Delegate to ProcessPayment with Lineage propagation. + // ProcessPayment inherits this workflow's full history so that its own + // child FraudDetection can verify the complete ancestor chain. + Console.WriteLine(" [MerchantCheckout] Step 2: ProcessPayment child wf (HistoryPropagationScope.Lineage)"); + var result = await ctx.CallChildWorkflowAsync( + nameof(ProcessPaymentWorkflow), + req, + new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); + + Console.WriteLine($" [MerchantCheckout] COMPLETE: {result}"); + return result; + } +} + +// --------------------------------------------------------------------------- +// ProcessPayment (child workflow, level 2) +// --------------------------------------------------------------------------- + +/// +/// Child workflow — orchestrates card validation, fraud detection, and +/// settlement. Receives from +/// MerchantCheckout, so it holds the full ancestor chain when calling its +/// own children. +/// +public sealed class ProcessPaymentWorkflow : Workflow +{ + public override async Task RunAsync(WorkflowContext ctx, PaymentRequest req) + { + Console.WriteLine($" [ProcessPayment] Starting payment ****{req.CardLast4} {req.Amount} {req.Currency}"); + + // Step 1: Validate card (no propagation). + Console.WriteLine(" [ProcessPayment] Step 1: ValidateCard (no propagation)"); + var cardValid = await ctx.CallActivityAsync( + nameof(ValidateCardActivity), + req); + if (!cardValid) + return "payment declined: invalid card"; + Console.WriteLine(" [ProcessPayment] Step 1 complete: card valid"); + + // Step 2: Check spending limits (no propagation). + Console.WriteLine(" [ProcessPayment] Step 2: CheckSpendingLimits (no propagation)"); + var withinLimits = await ctx.CallActivityAsync( + nameof(CheckSpendingLimitsActivity), + req); + if (!withinLimits) + return "payment declined: spending limit exceeded"; + Console.WriteLine(" [ProcessPayment] Step 2 complete: within limits"); + + // Step 3: Fraud detection grandchild workflow with Lineage propagation. + // FraudDetection will see both MerchantCheckout AND ProcessPayment events. + Console.WriteLine(" [ProcessPayment] Step 3: FraudDetection child wf (HistoryPropagationScope.Lineage)"); + var fraudResult = await ctx.CallChildWorkflowAsync( + nameof(FraudDetectionWorkflow), + req, + new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); + if (!fraudResult.Approved) + return $"payment declined: fraud check failed (risk={fraudResult.RiskScore:F2}, reason={fraudResult.Reason})"; + Console.WriteLine($" [ProcessPayment] Step 3 complete: fraud check passed (risk={fraudResult.RiskScore:F2})"); + + // Step 4: Settle payment as a child workflow with OwnHistory propagation. + // SettlementWorkflow only sees ProcessPayment's own events — not MerchantCheckout. + // Note: the .NET SDK propagation support is on ChildWorkflowTaskOptions only. + // For a trust-boundary demo equivalent to PropagationScope.OWN_HISTORY in Python, + // SettlePayment is implemented as a child workflow (not a bare activity). + Console.WriteLine(" [ProcessPayment] Step 4: SettlementWorkflow child wf (HistoryPropagationScope.OwnHistory)"); + var settlement = await ctx.CallChildWorkflowAsync( + nameof(SettlementWorkflow), + req, + new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); + Console.WriteLine($" [ProcessPayment] Step 4 complete: settled (txn={settlement.TransactionId})"); + + var summary = $"payment settled: txn={settlement.TransactionId}, " + + $"card=****{req.CardLast4}, amount={req.Amount} {req.Currency}"; + Console.WriteLine($" [ProcessPayment] COMPLETE: {summary}"); + return summary; + } +} + +// --------------------------------------------------------------------------- +// FraudDetection (grandchild workflow, level 3) +// --------------------------------------------------------------------------- + +/// +/// Grandchild workflow that inspects the full ancestor chain to make a +/// trust-aware fraud decision. Receives +/// from ProcessPayment, so returns +/// entries for both MerchantCheckout and ProcessPayment. +/// +public sealed class FraudDetectionWorkflow : Workflow +{ + public override Task RunAsync(WorkflowContext ctx, PaymentRequest req) + { + Console.WriteLine($" [FraudDetection] Checking payment ****{req.CardLast4} {req.Amount} {req.Currency}"); + + var history = ctx.GetPropagatedHistory(); + if (history is null) + { + Console.WriteLine(" [FraudDetection] WARNING: no propagated history — sidecar may not support 1.18+"); + return Task.FromResult(new FraudCheckResult( + RiskScore: 1.0, + Approved: false, + Reason: "no execution history provided — cannot verify caller pipeline", + EventCount: 0)); + } + + Console.WriteLine($" [FraudDetection] Received propagated history with {history.Entries.Count} segment(s):"); + foreach (var entry in history.Entries) + Console.WriteLine($" [FraudDetection] workflow: name={entry.WorkflowName} app={entry.AppId} events={entry.Events.Count}"); + + // Verify MerchantCheckout is present in the ancestor chain. + var merchantEntries = history.FilterByWorkflowName(nameof(MerchantCheckoutWorkflow)); + if (merchantEntries.Entries.Count == 0) + { + return Task.FromResult(new FraudCheckResult( + RiskScore: 0.9, + Approved: false, + Reason: $"{nameof(MerchantCheckoutWorkflow)} missing from propagated history", + EventCount: history.Entries.Count)); + } + + // Verify ProcessPayment is present in the ancestor chain. + var processEntries = history.FilterByWorkflowName(nameof(ProcessPaymentWorkflow)); + if (processEntries.Entries.Count == 0) + { + return Task.FromResult(new FraudCheckResult( + RiskScore: 0.9, + Approved: false, + Reason: $"{nameof(ProcessPaymentWorkflow)} missing from propagated history", + EventCount: history.Entries.Count)); + } + + // Verify the required activity completions are recorded in history events. + var merchantEntry = merchantEntries.Entries[0]; + var processEntry = processEntries.Entries[0]; + + int merchantCompletedCount = merchantEntry.Events.Count(e => e.Kind == HistoryEventKind.TaskCompleted); + int processCompletedCount = processEntry.Events.Count(e => e.Kind == HistoryEventKind.TaskCompleted); + + Console.WriteLine(" [FraudDetection] Verification:"); + Console.WriteLine($" MerchantCheckout TaskCompleted events: {merchantCompletedCount}"); + Console.WriteLine($" ProcessPayment TaskCompleted events: {processCompletedCount}"); + + if (merchantCompletedCount == 0 || processCompletedCount == 0) + { + return Task.FromResult(new FraudCheckResult( + RiskScore: 0.9, + Approved: false, + Reason: "required upstream checks not completed in propagated history", + EventCount: history.Entries.Count)); + } + + int totalEventCount = history.Entries.Sum(e => e.Events.Count); + double riskScore = req.Amount > 1000 ? 0.3 : 0.1; + Console.WriteLine($" [FraudDetection] APPROVED (risk={riskScore:F2}, total events inspected={totalEventCount})"); + + return Task.FromResult(new FraudCheckResult( + RiskScore: riskScore, + Approved: true, + Reason: "all upstream checks verified in propagated history", + EventCount: totalEventCount)); + } +} + +// --------------------------------------------------------------------------- +// SettlementWorkflow (grandchild workflow, level 3) +// --------------------------------------------------------------------------- + +/// +/// Settlement workflow — receives +/// from ProcessPayment, so it can only see ProcessPayment's own events. +/// This demonstrates the trust-boundary mode: the MerchantCheckout ancestor +/// history is intentionally excluded. +/// +/// +/// In the Python sibling this is implemented as a bare activity because the +/// Python SDK supports propagation= on call_activity(). The .NET +/// SDK's is currently scoped to child +/// workflows only (), so we use a child +/// workflow here to demonstrate the identical OwnHistory boundary. +/// +public sealed class SettlementWorkflow : Workflow +{ + public override async Task RunAsync(WorkflowContext ctx, PaymentRequest req) + { + var history = ctx.GetPropagatedHistory(); + + int eventCount = 0; + if (history is not null) + { + eventCount = history.Entries.Sum(e => e.Events.Count); + Console.WriteLine($" [SettlementWorkflow] Propagated segments: {history.Entries.Count}"); + foreach (var entry in history.Entries) + Console.WriteLine($" [SettlementWorkflow] workflow: name={entry.WorkflowName} app={entry.AppId} events={entry.Events.Count}"); + + // With OwnHistory, MerchantCheckout should NOT appear here. + var merchantEntries = history.FilterByWorkflowName(nameof(MerchantCheckoutWorkflow)); + Console.WriteLine($" [SettlementWorkflow] MerchantCheckout in history (expected 0): {merchantEntries.Entries.Count}"); + } + else + { + Console.WriteLine(" [SettlementWorkflow] No propagated history received"); + } + + var result = await ctx.CallActivityAsync( + nameof(SettlePaymentActivity), + req); + + Console.WriteLine($" [SettlementWorkflow] SETTLED: {result.TransactionId}"); + return result with { EventCount = eventCount }; + } +} From 0c715b53848a26dfaf5b86eb3e96ce8ad809a94c Mon Sep 17 00:00:00 2001 From: Nelson Parente Date: Wed, 20 May 2026 22:00:54 +0100 Subject: [PATCH 2/2] feat(dotnet): rewrite quickstart to patient-intake scenario Aligns the .NET workflow history propagation quickstart with the canonical Go reference (dapr/go-sdk#823, dapr/quickstarts#1315) so all SDK quickstarts share the same patient intake / e-prescribing scenario. - Swap credit-card/fraud scenario for patient-intake/e-prescribing - Adopt PatientIntake -> PrescribeMedication -> ComplianceAudit hierarchy - Add IsReplaying guards around all Console.WriteLine inside workflows - DispenseMedicationWorkflow still wraps the activity for OwnHistory (.NET SDK propagation is on ChildWorkflowTaskOptions only) - Add event-level history walking in DispenseMedicationWorkflow Signed-off-by: Nelson Parente --- .../csharp/sdk-context-propagation/README.md | 149 +++++---- .../order-processor/Activities.cs | 50 +-- .../order-processor/Models.cs | 50 +-- .../order-processor/Program.cs | 81 ++--- .../order-processor/Workflows.cs | 312 ++++++++++-------- 5 files changed, 355 insertions(+), 287 deletions(-) diff --git a/workflows/csharp/sdk-context-propagation/README.md b/workflows/csharp/sdk-context-propagation/README.md index 43826a3d0..b23b7614c 100644 --- a/workflows/csharp/sdk-context-propagation/README.md +++ b/workflows/csharp/sdk-context-propagation/README.md @@ -17,43 +17,47 @@ When a parent workflow calls a child workflow it can optionally attach a tamper- | **Own history** | `HistoryPropagationScope.OwnHistory` | Only the direct caller's events | | **Lineage** | `HistoryPropagationScope.Lineage` | Caller's events **plus** any ancestor history the caller itself received | -## Scenario: Credit-card payment with fraud detection +## Scenario: Patient intake / e-prescribing + +A compliance audit and a pharmacy dispense step refuse to act unless the propagated history proves the required upstream checks (insurance, allergies, drug interactions) actually ran. ``` -MerchantCheckout (root) - └─ ValidateMerchant (activity, no propagation) - └─ ProcessPayment (child wf, Lineage) - └─ ValidateCard (activity, no propagation) - └─ CheckSpendingLimits (activity, no propagation) - └─ FraudDetection (grandchild wf, Lineage) - | reads MerchantCheckout/ValidateMerchant - | ProcessPayment/ValidateCard - | ProcessPayment/CheckSpendingLimits - └─ SettlementWorkflow (grandchild wf, OwnHistory) - reads ProcessPayment events only - └─ SettlePayment (activity) +PatientIntake (root) + └─ VerifyInsurance (activity, no propagation) + └─ PrescribeMedication (child wf, Lineage) + └─ CheckAllergies (activity, no propagation) + └─ ScreenDrugInteractions (activity, no propagation) + └─ ComplianceAudit (grandchild wf, Lineage) + | reads PatientIntake/VerifyInsurance + | PrescribeMedication/CheckAllergies + | PrescribeMedication/ScreenDrugInteractions + └─ DispenseMedicationWorkflow (grandchild wf, OwnHistory) + reads PrescribeMedication events only + └─ DispenseMedication (activity) ``` -`FraudDetection` uses `HistoryPropagationScope.Lineage` to see the **full ancestor chain** — it can verify both the merchant validation (performed by the grandparent) and the card/limit checks (performed by the parent) before approving the transaction. +`ComplianceAudit` uses `HistoryPropagationScope.Lineage` to see the **full ancestor chain** — it can verify both the insurance check (performed by the grandparent `PatientIntake`) and the allergy/interaction checks (performed by the parent `PrescribeMedication`) before approving the prescription. + +`DispenseMedicationWorkflow` uses `HistoryPropagationScope.OwnHistory` to see only the **direct caller's events** — a trust-boundary mode that limits visibility to what `PrescribeMedication` itself executed. The pharmacy dispense system doesn't need (or get to see) the upstream patient-intake chain. -`SettlementWorkflow` uses `HistoryPropagationScope.OwnHistory` to see only the **direct caller's events** — a trust-boundary mode that limits visibility to what `ProcessPayment` itself executed. +This sample mirrors the canonical Go reference [dapr/go-sdk#823](https://github.com/dapr/go-sdk/pull/823) and the [Go quickstart](https://github.com/dapr/quickstarts/pull/1315). -### .NET vs Python difference +### .NET vs Python/Go difference -The Python sibling ([dapr/quickstarts#1309](https://github.com/dapr/quickstarts/pull/1309)) calls `settle_payment` as a bare activity with `propagation=PropagationScope.OWN_HISTORY`. In the .NET SDK (v1.18) `HistoryPropagationScope` is only available on `ChildWorkflowTaskOptions` — activity calls do not carry a propagation scope. To demonstrate the identical trust-boundary semantics, this sample wraps the settlement activity inside `SettlementWorkflow` (a child workflow). +The Python sibling ([dapr/quickstarts#1309](https://github.com/dapr/quickstarts/pull/1309)) and the Go reference call the final dispense step as a bare activity with an `OwnHistory` propagation argument. In the .NET SDK (v1.18) `HistoryPropagationScope` is only available on `ChildWorkflowTaskOptions` — activity calls do not carry a propagation scope. To demonstrate the identical trust-boundary semantics, this sample wraps the `DispenseMedicationActivity` inside `DispenseMedicationWorkflow` (a child workflow). ## .NET API surface ```csharp // Parent workflow — propagate Lineage when calling a child workflow var result = await ctx.CallChildWorkflowAsync( - nameof(FraudDetectionWorkflow), + nameof(ComplianceAuditWorkflow), input, new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); // Parent workflow — propagate OwnHistory when calling a child workflow -var settlement = await ctx.CallChildWorkflowAsync( - nameof(SettlementWorkflow), +var dispense = await ctx.CallChildWorkflowAsync( + nameof(DispenseMedicationWorkflow), input, new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); @@ -63,10 +67,10 @@ var history = ctx.GetPropagatedHistory(); // returns PropagatedHistory? if (history is not null) { // Filter to a specific ancestor workflow by name - var processEntries = history.FilterByWorkflowName(nameof(ProcessPaymentWorkflow)); + var prescribeEntries = history.FilterByWorkflowName(nameof(PrescribeMedicationWorkflow)); // Inspect events within that ancestor's segment - var completedCount = processEntries.Entries[0].Events + var completedCount = prescribeEntries.Entries[0].Events .Count(e => e.Kind == HistoryEventKind.TaskCompleted); } ``` @@ -79,6 +83,8 @@ Key types in `Dapr.Workflow`: - `PropagatedHistoryEvent` — has `EventId`, `Kind` (`HistoryEventKind`), `Timestamp` - `HistoryEventKind` — enum including `TaskScheduled`, `TaskCompleted`, `TaskFailed`, etc. +> **Replay safety**: workflow code runs many times during durable execution. Guard side-effecting calls — including `Console.WriteLine` — with `if (!ctx.IsReplaying)` so they only fire on the live execution, not on each replay. + ## Prerequisites - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) 1.18+ @@ -97,57 +103,66 @@ dapr run -f . ## Expected output ``` -============================================ -= WORKFLOW HISTORY PROPAGATION DEMO (.NET) = -============================================ - - Flow: MerchantCheckout -> ValidateMerchant - -> ProcessPayment (child wf, Lineage) - -> ValidateCard -> CheckSpendingLimits - -> FraudDetection (child wf, Lineage) <-- sees MerchantCheckout + ProcessPayment events - -> SettlementWorkflow (child wf, OwnHistory) <-- sees only ProcessPayment events - - [main] Scheduling workflow instance: checkout-001 - [MerchantCheckout] Starting checkout for merchant merchant-abc - [MerchantCheckout] Step 1: ValidateMerchant (no propagation) - [ValidateMerchant] Validating merchant merchant-abc - [MerchantCheckout] Step 1 complete: merchant valid - [MerchantCheckout] Step 2: ProcessPayment child wf (HistoryPropagationScope.Lineage) - [ProcessPayment] Starting payment ****4242 149.99 USD - [ProcessPayment] Step 1: ValidateCard (no propagation) - [ValidateCard] Validating card ****4242 - [ProcessPayment] Step 1 complete: card valid - [ProcessPayment] Step 2: CheckSpendingLimits (no propagation) - [CheckSpendingLimits] Checking 149.99 USD - [CheckSpendingLimits] Within limits: True - [ProcessPayment] Step 2 complete: within limits - [ProcessPayment] Step 3: FraudDetection child wf (HistoryPropagationScope.Lineage) - [FraudDetection] Checking payment ****4242 149.99 USD - [FraudDetection] Received propagated history with 2 segment(s): - [FraudDetection] workflow: name=MerchantCheckoutWorkflow app=order-processor events=... - [FraudDetection] workflow: name=ProcessPaymentWorkflow app=order-processor events=... - [FraudDetection] Verification: - MerchantCheckout TaskCompleted events: 1 - ProcessPayment TaskCompleted events: 2 - [FraudDetection] APPROVED (risk=0.10, total events inspected=...) - [ProcessPayment] Step 3 complete: fraud check passed (risk=0.10) - [ProcessPayment] Step 4: SettlementWorkflow child wf (HistoryPropagationScope.OwnHistory) - [SettlementWorkflow] Propagated segments: 1 - [SettlementWorkflow] workflow: name=ProcessPaymentWorkflow app=order-processor events=... - [SettlementWorkflow] MerchantCheckout in history (expected 0): 0 - [SettlePayment] SETTLED: txn-merchant-abc-... - [MerchantCheckout] COMPLETE: payment settled: txn=txn-merchant-abc-..., card=****4242, amount=149.99 USD - [main] Workflow completed! Output: "payment settled: ..." - -============================================ -= COMPLETE = -============================================ +================================================================ += WORKFLOW HISTORY PROPAGATION DEMO — PATIENT INTAKE (.NET) = +================================================================ + + Flow: PatientIntake -> VerifyInsurance + -> PrescribeMedication (child wf, Lineage) + -> CheckAllergies -> ScreenDrugInteractions + -> ComplianceAudit (child wf, Lineage) <-- sees PatientIntake + PrescribeMedication events + -> DispenseMedicationWorkflow (child wf, OwnHistory) <-- sees only PrescribeMedication events + + [main] Scheduling workflow instance: intake-001 + [PatientIntake] Starting intake for patient P-1042 + [PatientIntake] Step 1: VerifyInsurance (no propagation) + [VerifyInsurance] Checking coverage for patient P-1042 + [PatientIntake] Step 1 complete: insurance verified + [PatientIntake] Step 2: PrescribeMedication child wf (HistoryPropagationScope.Lineage) + [PrescribeMedication] Starting prescription: amoxicillin 500mg for bacterial sinusitis + [PrescribeMedication] Step 1: CheckAllergies (no propagation) + [CheckAllergies] Screening P-1042 for amoxicillin + [PrescribeMedication] Step 1 complete: allergy clear + [PrescribeMedication] Step 2: ScreenDrugInteractions (no propagation) + [ScreenDrugInteractions] Screening amoxicillin 500mg for P-1042 + [PrescribeMedication] Step 2 complete: no interactions + [PrescribeMedication] Step 3: ComplianceAudit child wf (HistoryPropagationScope.Lineage) + [ComplianceAudit] Auditing prescription for patient P-1042 + [ComplianceAudit] Received propagated history with 2 segment(s): + [ComplianceAudit] workflow: name=PatientIntakeWorkflow app=order-processor events=... + [ComplianceAudit] workflow: name=PrescribeMedicationWorkflow app=order-processor events=... + [ComplianceAudit] Verification: + PatientIntake TaskCompleted events: 1 (expect >= 1: VerifyInsurance) + PrescribeMedication TaskCompleted events: 2 (expect >= 2: CheckAllergies, ScreenDrugInteractions) + [ComplianceAudit] APPROVED (risk=0.10, total events inspected=...) + [PrescribeMedication] Step 3 complete: compliance audit passed (risk=0.10) + [PrescribeMedication] Step 4: DispenseMedicationWorkflow child wf (HistoryPropagationScope.OwnHistory) + [DispenseMedicationWorkflow] Propagated segments: 1 + [DispenseMedicationWorkflow] workflow: name=PrescribeMedicationWorkflow app=order-processor events=... + [DispenseMedicationWorkflow] event: kind=ExecutionStarted id=... + [DispenseMedicationWorkflow] event: kind=TaskScheduled id=... + [DispenseMedicationWorkflow] event: kind=TaskCompleted id=... + [DispenseMedicationWorkflow] PatientIntake in history (expected 0): 0 + [DispenseMedication] DISPENSED: rx-P-1042-... (amoxicillin 500mg) + [PrescribeMedication] Step 4 complete: dispensed (id=rx-P-1042-...) + [PrescribeMedication] COMPLETE: dispensed: id=rx-P-1042-..., patient=P-1042, drug=amoxicillin 500mg + [PatientIntake] COMPLETE: dispensed: id=rx-P-1042-..., patient=P-1042, drug=amoxicillin 500mg + [main] Workflow completed! Output: "dispensed: ..." + +================================================================ += COMPLETE = +================================================================ ``` +## Standalone-mode note + +In standalone mode the sidecar will log `propagating unsigned workflow history to ...` warnings — these are expected. Without `WorkflowHistorySigning` enabled, propagated history chunks aren't cryptographically signed, which is fine for a local `dapr run` demo. Signing the chunks within an mTLS trust boundary is a production concern handled at the cluster/control-plane level and is out of scope for this quickstart. + ## References - Sibling Python quickstart: [dapr/quickstarts#1309](https://github.com/dapr/quickstarts/pull/1309) - Canonical Go SDK reference: [dapr/go-sdk#823](https://github.com/dapr/go-sdk/pull/823) +- Sibling Go quickstart: [dapr/quickstarts#1315](https://github.com/dapr/quickstarts/pull/1315) - .NET SDK implementation: [dapr/dotnet-sdk#1802](https://github.com/dapr/dotnet-sdk/pull/1802) - Runtime support: [dapr/dapr#9810](https://github.com/dapr/dapr/pull/9810) - Docs (.NET): [dapr/docs#5174](https://github.com/dapr/docs/pull/5174) diff --git a/workflows/csharp/sdk-context-propagation/order-processor/Activities.cs b/workflows/csharp/sdk-context-propagation/order-processor/Activities.cs index 7e5bdf167..2403890ef 100644 --- a/workflows/csharp/sdk-context-propagation/order-processor/Activities.cs +++ b/workflows/csharp/sdk-context-propagation/order-processor/Activities.cs @@ -16,56 +16,56 @@ namespace OrderProcessor; using Dapr.Workflow; /// -/// Validates the merchant account. Called by MerchantCheckout without propagation. +/// Verifies the patient's insurance coverage. Called by PatientIntake without +/// propagation. /// -public sealed class ValidateMerchantActivity : WorkflowActivity +public sealed class VerifyInsuranceActivity : WorkflowActivity { - public override Task RunAsync(WorkflowActivityContext ctx, PaymentRequest req) + public override Task RunAsync(WorkflowActivityContext ctx, PatientRecord rec) { - Console.WriteLine($" [ValidateMerchant] Validating merchant {req.MerchantId}"); + Console.WriteLine($" [VerifyInsurance] Checking coverage for patient {rec.PatientId}"); return Task.FromResult(true); } } /// -/// Validates the payment card. Called by ProcessPayment without propagation. +/// Screens the patient against their allergy list for the candidate drug. +/// Called by PrescribeMedication without propagation. /// -public sealed class ValidateCardActivity : WorkflowActivity +public sealed class CheckAllergiesActivity : WorkflowActivity { - public override Task RunAsync(WorkflowActivityContext ctx, PaymentRequest req) + public override Task RunAsync(WorkflowActivityContext ctx, PatientRecord rec) { - Console.WriteLine($" [ValidateCard] Validating card ****{req.CardLast4}"); + Console.WriteLine($" [CheckAllergies] Screening {rec.PatientId} for {rec.Medication}"); return Task.FromResult(true); } } /// -/// Checks that the payment amount is within card spending limits. -/// Called by ProcessPayment without propagation. +/// Screens the candidate prescription against the patient's active medication +/// list. Called by PrescribeMedication without propagation. /// -public sealed class CheckSpendingLimitsActivity : WorkflowActivity +public sealed class ScreenDrugInteractionsActivity : WorkflowActivity { - public override Task RunAsync(WorkflowActivityContext ctx, PaymentRequest req) + public override Task RunAsync(WorkflowActivityContext ctx, PatientRecord rec) { - Console.WriteLine($" [CheckSpendingLimits] Checking {req.Amount} {req.Currency}"); - bool withinLimits = req.Amount <= 10_000; - Console.WriteLine($" [CheckSpendingLimits] Within limits: {withinLimits}"); - return Task.FromResult(withinLimits); + Console.WriteLine($" [ScreenDrugInteractions] Screening {rec.Medication} {rec.Dosage:F0}mg for {rec.PatientId}"); + return Task.FromResult(true); } } /// -/// Executes the final payment settlement. Called by SettlementWorkflow. +/// Dispenses the medication. Called by DispenseMedicationWorkflow. /// -public sealed class SettlePaymentActivity : WorkflowActivity +public sealed class DispenseMedicationActivity : WorkflowActivity { - public override Task RunAsync(WorkflowActivityContext ctx, PaymentRequest req) + public override Task RunAsync(WorkflowActivityContext ctx, PatientRecord rec) { - var txnId = $"txn-{req.MerchantId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; - Console.WriteLine($" [SettlePayment] SETTLED: {txnId}"); - return Task.FromResult(new SettlementResult( - TransactionId: txnId, - Status: "settled", - EventCount: 0)); // EventCount populated by SettlementWorkflow + var dispenseId = $"rx-{rec.PatientId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; + Console.WriteLine($" [DispenseMedication] DISPENSED: {dispenseId} ({rec.Medication} {rec.Dosage:F0}mg)"); + return Task.FromResult(new DispenseResult( + DispenseId: dispenseId, + Status: "dispensed", + EventCount: 0)); // EventCount populated by DispenseMedicationWorkflow } } diff --git a/workflows/csharp/sdk-context-propagation/order-processor/Models.cs b/workflows/csharp/sdk-context-propagation/order-processor/Models.cs index 8c54f5e34..5dd69534e 100644 --- a/workflows/csharp/sdk-context-propagation/order-processor/Models.cs +++ b/workflows/csharp/sdk-context-propagation/order-processor/Models.cs @@ -14,36 +14,42 @@ namespace OrderProcessor; /// -/// Payment request passed through the workflow hierarchy. +/// Patient record propagated through the workflow hierarchy. In a real +/// deployment the Name / DOB / MRN fields are protected health info and +/// would be candidates for redaction when the record is propagated downstream. /// -/// Last four digits of the payment card. -/// Amount to charge. -/// ISO 4217 currency code. -/// Merchant identifier. -/// Human-readable payment description. -public sealed record PaymentRequest( - string CardLast4, - double Amount, - string Currency, - string MerchantId, - string Description); +/// Patient identifier. +/// Patient name. +/// Date of birth (YYYY-MM-DD). +/// Medical record number. +/// Diagnosis / indication. +/// Prescribed drug name. +/// Dosage in milligrams. +public sealed record PatientRecord( + string PatientId, + string Name, + string Dob, + string Mrn, + string Condition, + string Medication, + double Dosage); -/// Result produced by the FraudDetection workflow. +/// Result produced by the ComplianceAudit workflow. +/// Whether the prescription cleared compliance. /// Risk score in the range [0, 1]. -/// Whether the transaction was approved. /// Human-readable decision rationale. -/// Number of propagated history events inspected. -public sealed record FraudCheckResult( +/// Number of propagated history segments inspected. +public sealed record ComplianceResult( + bool Compliant, double RiskScore, - bool Approved, string Reason, int EventCount); -/// Result produced by the SettlePayment activity. -/// Unique transaction reference. -/// Settlement status string. +/// Result produced by the DispenseMedication activity. +/// Pharmacy dispense identifier. +/// Dispense status string. /// Number of propagated history events inspected. -public sealed record SettlementResult( - string TransactionId, +public sealed record DispenseResult( + string DispenseId, string Status, int EventCount); diff --git a/workflows/csharp/sdk-context-propagation/order-processor/Program.cs b/workflows/csharp/sdk-context-propagation/order-processor/Program.cs index 6c98e0e0b..3fae6514c 100644 --- a/workflows/csharp/sdk-context-propagation/order-processor/Program.cs +++ b/workflows/csharp/sdk-context-propagation/order-processor/Program.cs @@ -13,18 +13,19 @@ // Workflow History Propagation Quickstart (.NET SDK) // -// Scenario: credit-card payment processing with fraud detection. +// Scenario: patient intake / e-prescribing pipeline. // // Flow: -// MerchantCheckout (root) -// └─ ValidateMerchant (activity, no propagation) -// └─ ProcessPayment (child wf, HistoryPropagationScope.Lineage) -// └─ ValidateCard (activity, no propagation) -// └─ CheckSpendingLimits (activity, no propagation) -// └─ FraudDetection (grandchild wf, HistoryPropagationScope.Lineage) -// | reads: MerchantCheckout + ProcessPayment events -// └─ SettlePayment (activity, HistoryPropagationScope.OwnHistory) -// reads: ProcessPayment events only +// PatientIntake (root) +// └─ VerifyInsurance (activity, no propagation) +// └─ PrescribeMedication (child wf, HistoryPropagationScope.Lineage) +// └─ CheckAllergies (activity, no propagation) +// └─ ScreenDrugInteractions (activity, no propagation) +// └─ ComplianceAudit (grandchild wf, HistoryPropagationScope.Lineage) +// | reads: PatientIntake + PrescribeMedication events +// └─ DispenseMedicationWorkflow (grandchild wf, HistoryPropagationScope.OwnHistory) +// reads: PrescribeMedication events only +// └─ DispenseMedication (activity) // // Requires Dapr 1.18+ (dapr/dapr#9810) and Dapr.Workflow 1.18+ (dapr/dotnet-sdk#1802). // Against an older sidecar GetPropagatedHistory() returns null and the sample @@ -37,9 +38,9 @@ using OrderProcessor; const string Banner = - "============================================\n" + - "= WORKFLOW HISTORY PROPAGATION DEMO (.NET) =\n" + - "============================================"; + "================================================================\n" + + "= WORKFLOW HISTORY PROPAGATION DEMO — PATIENT INTAKE (.NET) =\n" + + "================================================================"; // --------------------------------------------------------------------------- // Host setup — register workflows and activities @@ -51,15 +52,15 @@ services.AddDaprClient(); services.AddDaprWorkflow(options => { - options.RegisterWorkflow(); - options.RegisterWorkflow(); - options.RegisterWorkflow(); - options.RegisterWorkflow(); - - options.RegisterActivity(); - options.RegisterActivity(); - options.RegisterActivity(); - options.RegisterActivity(); + options.RegisterWorkflow(); + options.RegisterWorkflow(); + options.RegisterWorkflow(); + options.RegisterWorkflow(); + + options.RegisterActivity(); + options.RegisterActivity(); + options.RegisterActivity(); + options.RegisterActivity(); }); }); @@ -74,28 +75,30 @@ Console.WriteLine(Banner); Console.WriteLine(); -Console.WriteLine(" Flow: MerchantCheckout -> ValidateMerchant"); -Console.WriteLine(" -> ProcessPayment (child wf, Lineage)"); -Console.WriteLine(" -> ValidateCard -> CheckSpendingLimits"); -Console.WriteLine(" -> FraudDetection (child wf, Lineage) <-- sees MerchantCheckout + ProcessPayment events"); -Console.WriteLine(" -> SettlePayment (activity, OwnHistory) <-- sees only ProcessPayment events"); +Console.WriteLine(" Flow: PatientIntake -> VerifyInsurance"); +Console.WriteLine(" -> PrescribeMedication (child wf, Lineage)"); +Console.WriteLine(" -> CheckAllergies -> ScreenDrugInteractions"); +Console.WriteLine(" -> ComplianceAudit (child wf, Lineage) <-- sees PatientIntake + PrescribeMedication events"); +Console.WriteLine(" -> DispenseMedicationWorkflow (child wf, OwnHistory) <-- sees only PrescribeMedication events"); Console.WriteLine(); -var request = new PaymentRequest( - CardLast4: "4242", - Amount: 149.99, - Currency: "USD", - MerchantId: "merchant-abc", - Description: "Online purchase"); +var record = new PatientRecord( + PatientId: "P-1042", + Name: "Jane Doe", + Dob: "1985-06-12", + Mrn: "MRN-77231", + Condition: "bacterial sinusitis", + Medication: "amoxicillin", + Dosage: 500); -const string InstanceId = "checkout-001"; +const string InstanceId = "intake-001"; Console.WriteLine($" [main] Scheduling workflow instance: {InstanceId}"); await workflowClient.ScheduleNewWorkflowAsync( - name: nameof(MerchantCheckoutWorkflow), + name: nameof(PatientIntakeWorkflow), instanceId: InstanceId, - input: request); + input: record); var state = await workflowClient.WaitForWorkflowCompletionAsync( instanceId: InstanceId, @@ -115,6 +118,6 @@ await workflowClient.ScheduleNewWorkflowAsync( } Console.WriteLine(); -Console.WriteLine("============================================"); -Console.WriteLine("= COMPLETE ="); -Console.WriteLine("============================================"); +Console.WriteLine("================================================================"); +Console.WriteLine("= COMPLETE ="); +Console.WriteLine("================================================================"); diff --git a/workflows/csharp/sdk-context-propagation/order-processor/Workflows.cs b/workflows/csharp/sdk-context-propagation/order-processor/Workflows.cs index d2f0dd765..db07aa015 100644 --- a/workflows/csharp/sdk-context-propagation/order-processor/Workflows.cs +++ b/workflows/csharp/sdk-context-propagation/order-processor/Workflows.cs @@ -16,210 +16,246 @@ namespace OrderProcessor; using Dapr.Workflow; // --------------------------------------------------------------------------- -// MerchantCheckout (root workflow) +// PatientIntake (root workflow) // --------------------------------------------------------------------------- /// -/// Root workflow — validates the merchant then delegates payment to a child -/// workflow with full propagation -/// so the grandchild FraudDetection can inspect the complete ancestor chain. +/// Root workflow — verifies the patient's insurance then delegates the +/// prescription to a child workflow with full +/// propagation so the grandchild +/// ComplianceAudit can inspect the complete ancestor chain. /// -public sealed class MerchantCheckoutWorkflow : Workflow +public sealed class PatientIntakeWorkflow : Workflow { - public override async Task RunAsync(WorkflowContext ctx, PaymentRequest req) + public override async Task RunAsync(WorkflowContext ctx, PatientRecord rec) { - Console.WriteLine($" [MerchantCheckout] Starting checkout for merchant {req.MerchantId}"); - - // Step 1: Validate merchant — no propagation (plain activity). - Console.WriteLine(" [MerchantCheckout] Step 1: ValidateMerchant (no propagation)"); - await ctx.CallActivityAsync( - nameof(ValidateMerchantActivity), - req); - Console.WriteLine(" [MerchantCheckout] Step 1 complete: merchant valid"); - - // Step 2: Delegate to ProcessPayment with Lineage propagation. - // ProcessPayment inherits this workflow's full history so that its own - // child FraudDetection can verify the complete ancestor chain. - Console.WriteLine(" [MerchantCheckout] Step 2: ProcessPayment child wf (HistoryPropagationScope.Lineage)"); + if (!ctx.IsReplaying) + Console.WriteLine($" [PatientIntake] Starting intake for patient {rec.PatientId}"); + + // Step 1: Verify insurance — no propagation (plain activity). + if (!ctx.IsReplaying) + Console.WriteLine(" [PatientIntake] Step 1: VerifyInsurance (no propagation)"); + var insured = await ctx.CallActivityAsync( + nameof(VerifyInsuranceActivity), + rec); + if (!insured) + return "intake declined: insurance not on file"; + if (!ctx.IsReplaying) + Console.WriteLine(" [PatientIntake] Step 1 complete: insurance verified"); + + // Step 2: Delegate to PrescribeMedication with Lineage propagation. + // PrescribeMedication inherits this workflow's full history so its own + // grandchild ComplianceAudit can verify the complete ancestor chain. + if (!ctx.IsReplaying) + Console.WriteLine(" [PatientIntake] Step 2: PrescribeMedication child wf (HistoryPropagationScope.Lineage)"); var result = await ctx.CallChildWorkflowAsync( - nameof(ProcessPaymentWorkflow), - req, + nameof(PrescribeMedicationWorkflow), + rec, new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); - Console.WriteLine($" [MerchantCheckout] COMPLETE: {result}"); + if (!ctx.IsReplaying) + Console.WriteLine($" [PatientIntake] COMPLETE: {result}"); return result; } } // --------------------------------------------------------------------------- -// ProcessPayment (child workflow, level 2) +// PrescribeMedication (child workflow, level 2) // --------------------------------------------------------------------------- /// -/// Child workflow — orchestrates card validation, fraud detection, and -/// settlement. Receives from -/// MerchantCheckout, so it holds the full ancestor chain when calling its -/// own children. +/// Child workflow — orchestrates allergy + interaction screening, compliance +/// audit, and dispensing. Receives +/// from PatientIntake, so it holds the full ancestor chain when calling its +/// own children. Calls ComplianceAudit with Lineage (audit needs to see the +/// grandparent) and DispenseMedicationWorkflow with OwnHistory (pharmacy only +/// sees the prescribing step, not the intake). /// -public sealed class ProcessPaymentWorkflow : Workflow +public sealed class PrescribeMedicationWorkflow : Workflow { - public override async Task RunAsync(WorkflowContext ctx, PaymentRequest req) + public override async Task RunAsync(WorkflowContext ctx, PatientRecord rec) { - Console.WriteLine($" [ProcessPayment] Starting payment ****{req.CardLast4} {req.Amount} {req.Currency}"); - - // Step 1: Validate card (no propagation). - Console.WriteLine(" [ProcessPayment] Step 1: ValidateCard (no propagation)"); - var cardValid = await ctx.CallActivityAsync( - nameof(ValidateCardActivity), - req); - if (!cardValid) - return "payment declined: invalid card"; - Console.WriteLine(" [ProcessPayment] Step 1 complete: card valid"); - - // Step 2: Check spending limits (no propagation). - Console.WriteLine(" [ProcessPayment] Step 2: CheckSpendingLimits (no propagation)"); - var withinLimits = await ctx.CallActivityAsync( - nameof(CheckSpendingLimitsActivity), - req); - if (!withinLimits) - return "payment declined: spending limit exceeded"; - Console.WriteLine(" [ProcessPayment] Step 2 complete: within limits"); - - // Step 3: Fraud detection grandchild workflow with Lineage propagation. - // FraudDetection will see both MerchantCheckout AND ProcessPayment events. - Console.WriteLine(" [ProcessPayment] Step 3: FraudDetection child wf (HistoryPropagationScope.Lineage)"); - var fraudResult = await ctx.CallChildWorkflowAsync( - nameof(FraudDetectionWorkflow), - req, + if (!ctx.IsReplaying) + Console.WriteLine($" [PrescribeMedication] Starting prescription: {rec.Medication} {rec.Dosage:F0}mg for {rec.Condition}"); + + // Step 1: Allergy check (no propagation). + if (!ctx.IsReplaying) + Console.WriteLine(" [PrescribeMedication] Step 1: CheckAllergies (no propagation)"); + var allergyClear = await ctx.CallActivityAsync( + nameof(CheckAllergiesActivity), + rec); + if (!allergyClear) + return "prescription declined: known allergy"; + if (!ctx.IsReplaying) + Console.WriteLine(" [PrescribeMedication] Step 1 complete: allergy clear"); + + // Step 2: Drug interaction screen (no propagation). + if (!ctx.IsReplaying) + Console.WriteLine(" [PrescribeMedication] Step 2: ScreenDrugInteractions (no propagation)"); + var interactionsClear = await ctx.CallActivityAsync( + nameof(ScreenDrugInteractionsActivity), + rec); + if (!interactionsClear) + return "prescription declined: drug interaction risk"; + if (!ctx.IsReplaying) + Console.WriteLine(" [PrescribeMedication] Step 2 complete: no interactions"); + + // Step 3: Compliance audit grandchild workflow with Lineage propagation. + // ComplianceAudit will see both PatientIntake AND PrescribeMedication events. + if (!ctx.IsReplaying) + Console.WriteLine(" [PrescribeMedication] Step 3: ComplianceAudit child wf (HistoryPropagationScope.Lineage)"); + var audit = await ctx.CallChildWorkflowAsync( + nameof(ComplianceAuditWorkflow), + rec, new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); - if (!fraudResult.Approved) - return $"payment declined: fraud check failed (risk={fraudResult.RiskScore:F2}, reason={fraudResult.Reason})"; - Console.WriteLine($" [ProcessPayment] Step 3 complete: fraud check passed (risk={fraudResult.RiskScore:F2})"); + if (!audit.Compliant) + return $"prescription blocked: compliance audit failed (risk={audit.RiskScore:F2}, reason={audit.Reason})"; + if (!ctx.IsReplaying) + Console.WriteLine($" [PrescribeMedication] Step 3 complete: compliance audit passed (risk={audit.RiskScore:F2})"); - // Step 4: Settle payment as a child workflow with OwnHistory propagation. - // SettlementWorkflow only sees ProcessPayment's own events — not MerchantCheckout. + // Step 4: Dispense the medication as a child workflow with OwnHistory propagation. + // DispenseMedicationWorkflow only sees PrescribeMedication's own events — not PatientIntake. // Note: the .NET SDK propagation support is on ChildWorkflowTaskOptions only. - // For a trust-boundary demo equivalent to PropagationScope.OWN_HISTORY in Python, - // SettlePayment is implemented as a child workflow (not a bare activity). - Console.WriteLine(" [ProcessPayment] Step 4: SettlementWorkflow child wf (HistoryPropagationScope.OwnHistory)"); - var settlement = await ctx.CallChildWorkflowAsync( - nameof(SettlementWorkflow), - req, + // For a trust-boundary demo equivalent to PropagationScope.OWN_HISTORY in Python/Go, + // DispenseMedication is implemented as a child workflow (not a bare activity). + if (!ctx.IsReplaying) + Console.WriteLine(" [PrescribeMedication] Step 4: DispenseMedicationWorkflow child wf (HistoryPropagationScope.OwnHistory)"); + var dispense = await ctx.CallChildWorkflowAsync( + nameof(DispenseMedicationWorkflow), + rec, new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.OwnHistory)); - Console.WriteLine($" [ProcessPayment] Step 4 complete: settled (txn={settlement.TransactionId})"); + if (!ctx.IsReplaying) + Console.WriteLine($" [PrescribeMedication] Step 4 complete: dispensed (id={dispense.DispenseId})"); - var summary = $"payment settled: txn={settlement.TransactionId}, " + - $"card=****{req.CardLast4}, amount={req.Amount} {req.Currency}"; - Console.WriteLine($" [ProcessPayment] COMPLETE: {summary}"); + var summary = $"dispensed: id={dispense.DispenseId}, patient={rec.PatientId}, drug={rec.Medication} {rec.Dosage:F0}mg"; + if (!ctx.IsReplaying) + Console.WriteLine($" [PrescribeMedication] COMPLETE: {summary}"); return summary; } } // --------------------------------------------------------------------------- -// FraudDetection (grandchild workflow, level 3) +// ComplianceAudit (grandchild workflow, level 3) // --------------------------------------------------------------------------- /// /// Grandchild workflow that inspects the full ancestor chain to make a -/// trust-aware fraud decision. Receives -/// from ProcessPayment, so returns -/// entries for both MerchantCheckout and ProcessPayment. +/// trust-aware compliance decision. Receives +/// from PrescribeMedication, so +/// returns entries for both PatientIntake and PrescribeMedication. Refuses to +/// approve dispensing unless the required upstream steps (insurance, allergies, +/// drug interactions) are all present and completed in the propagated history. /// -public sealed class FraudDetectionWorkflow : Workflow +public sealed class ComplianceAuditWorkflow : Workflow { - public override Task RunAsync(WorkflowContext ctx, PaymentRequest req) + public override Task RunAsync(WorkflowContext ctx, PatientRecord rec) { - Console.WriteLine($" [FraudDetection] Checking payment ****{req.CardLast4} {req.Amount} {req.Currency}"); + if (!ctx.IsReplaying) + Console.WriteLine($" [ComplianceAudit] Auditing prescription for patient {rec.PatientId}"); var history = ctx.GetPropagatedHistory(); if (history is null) { - Console.WriteLine(" [FraudDetection] WARNING: no propagated history — sidecar may not support 1.18+"); - return Task.FromResult(new FraudCheckResult( + if (!ctx.IsReplaying) + { + Console.WriteLine(" [ComplianceAudit] WARNING: no propagated history — sidecar may not support 1.18+"); + Console.WriteLine(" [ComplianceAudit] BLOCKED — cannot verify upstream pipeline without history"); + } + return Task.FromResult(new ComplianceResult( + Compliant: false, RiskScore: 1.0, - Approved: false, Reason: "no execution history provided — cannot verify caller pipeline", EventCount: 0)); } - Console.WriteLine($" [FraudDetection] Received propagated history with {history.Entries.Count} segment(s):"); - foreach (var entry in history.Entries) - Console.WriteLine($" [FraudDetection] workflow: name={entry.WorkflowName} app={entry.AppId} events={entry.Events.Count}"); + if (!ctx.IsReplaying) + { + Console.WriteLine($" [ComplianceAudit] Received propagated history with {history.Entries.Count} segment(s):"); + foreach (var entry in history.Entries) + Console.WriteLine($" [ComplianceAudit] workflow: name={entry.WorkflowName} app={entry.AppId} events={entry.Events.Count}"); + } - // Verify MerchantCheckout is present in the ancestor chain. - var merchantEntries = history.FilterByWorkflowName(nameof(MerchantCheckoutWorkflow)); - if (merchantEntries.Entries.Count == 0) + // Verify PatientIntake is present in the ancestor chain. + var intakeEntries = history.FilterByWorkflowName(nameof(PatientIntakeWorkflow)); + if (intakeEntries.Entries.Count == 0) { - return Task.FromResult(new FraudCheckResult( + return Task.FromResult(new ComplianceResult( + Compliant: false, RiskScore: 0.9, - Approved: false, - Reason: $"{nameof(MerchantCheckoutWorkflow)} missing from propagated history", + Reason: $"{nameof(PatientIntakeWorkflow)} missing from propagated history", EventCount: history.Entries.Count)); } - // Verify ProcessPayment is present in the ancestor chain. - var processEntries = history.FilterByWorkflowName(nameof(ProcessPaymentWorkflow)); - if (processEntries.Entries.Count == 0) + // Verify PrescribeMedication is present in the ancestor chain. + var prescribeEntries = history.FilterByWorkflowName(nameof(PrescribeMedicationWorkflow)); + if (prescribeEntries.Entries.Count == 0) { - return Task.FromResult(new FraudCheckResult( + return Task.FromResult(new ComplianceResult( + Compliant: false, RiskScore: 0.9, - Approved: false, - Reason: $"{nameof(ProcessPaymentWorkflow)} missing from propagated history", + Reason: $"{nameof(PrescribeMedicationWorkflow)} missing from propagated history", EventCount: history.Entries.Count)); } // Verify the required activity completions are recorded in history events. - var merchantEntry = merchantEntries.Entries[0]; - var processEntry = processEntries.Entries[0]; + var intakeEntry = intakeEntries.Entries[0]; + var prescribeEntry = prescribeEntries.Entries[0]; - int merchantCompletedCount = merchantEntry.Events.Count(e => e.Kind == HistoryEventKind.TaskCompleted); - int processCompletedCount = processEntry.Events.Count(e => e.Kind == HistoryEventKind.TaskCompleted); + int intakeCompletedCount = intakeEntry.Events.Count(e => e.Kind == HistoryEventKind.TaskCompleted); + int prescribeCompletedCount = prescribeEntry.Events.Count(e => e.Kind == HistoryEventKind.TaskCompleted); - Console.WriteLine(" [FraudDetection] Verification:"); - Console.WriteLine($" MerchantCheckout TaskCompleted events: {merchantCompletedCount}"); - Console.WriteLine($" ProcessPayment TaskCompleted events: {processCompletedCount}"); + if (!ctx.IsReplaying) + { + Console.WriteLine(" [ComplianceAudit] Verification:"); + Console.WriteLine($" PatientIntake TaskCompleted events: {intakeCompletedCount} (expect >= 1: VerifyInsurance)"); + Console.WriteLine($" PrescribeMedication TaskCompleted events: {prescribeCompletedCount} (expect >= 2: CheckAllergies, ScreenDrugInteractions)"); + } - if (merchantCompletedCount == 0 || processCompletedCount == 0) + if (intakeCompletedCount == 0 || prescribeCompletedCount < 2) { - return Task.FromResult(new FraudCheckResult( + if (!ctx.IsReplaying) + Console.WriteLine(" [ComplianceAudit] BLOCKED — required upstream checks not completed"); + return Task.FromResult(new ComplianceResult( + Compliant: false, RiskScore: 0.9, - Approved: false, Reason: "required upstream checks not completed in propagated history", EventCount: history.Entries.Count)); } int totalEventCount = history.Entries.Sum(e => e.Events.Count); - double riskScore = req.Amount > 1000 ? 0.3 : 0.1; - Console.WriteLine($" [FraudDetection] APPROVED (risk={riskScore:F2}, total events inspected={totalEventCount})"); + double riskScore = rec.Dosage > 1000 ? 0.3 : 0.1; + if (!ctx.IsReplaying) + Console.WriteLine($" [ComplianceAudit] APPROVED (risk={riskScore:F2}, total events inspected={totalEventCount})"); - return Task.FromResult(new FraudCheckResult( + return Task.FromResult(new ComplianceResult( + Compliant: true, RiskScore: riskScore, - Approved: true, Reason: "all upstream checks verified in propagated history", EventCount: totalEventCount)); } } // --------------------------------------------------------------------------- -// SettlementWorkflow (grandchild workflow, level 3) +// DispenseMedicationWorkflow (grandchild workflow, level 3) // --------------------------------------------------------------------------- /// -/// Settlement workflow — receives -/// from ProcessPayment, so it can only see ProcessPayment's own events. -/// This demonstrates the trust-boundary mode: the MerchantCheckout ancestor -/// history is intentionally excluded. +/// Dispense workflow — receives +/// from PrescribeMedication, so it can only see PrescribeMedication's own events. +/// This demonstrates the trust-boundary mode: the PatientIntake ancestor history +/// is intentionally excluded — the pharmacy system doesn't need (or get to see) +/// the upstream patient-intake chain. /// /// -/// In the Python sibling this is implemented as a bare activity because the -/// Python SDK supports propagation= on call_activity(). The .NET -/// SDK's is currently scoped to child -/// workflows only (), so we use a child -/// workflow here to demonstrate the identical OwnHistory boundary. +/// In the Python sibling and the Go reference this is implemented as a bare +/// activity because those SDKs support a propagation argument on activity calls. +/// The .NET SDK's is currently scoped to +/// child workflows only (), so we use a +/// child workflow here to demonstrate the identical OwnHistory boundary. /// -public sealed class SettlementWorkflow : Workflow +public sealed class DispenseMedicationWorkflow : Workflow { - public override async Task RunAsync(WorkflowContext ctx, PaymentRequest req) + public override async Task RunAsync(WorkflowContext ctx, PatientRecord rec) { var history = ctx.GetPropagatedHistory(); @@ -227,24 +263,32 @@ public override async Task RunAsync(WorkflowContext ctx, Payme if (history is not null) { eventCount = history.Entries.Sum(e => e.Events.Count); - Console.WriteLine($" [SettlementWorkflow] Propagated segments: {history.Entries.Count}"); - foreach (var entry in history.Entries) - Console.WriteLine($" [SettlementWorkflow] workflow: name={entry.WorkflowName} app={entry.AppId} events={entry.Events.Count}"); - - // With OwnHistory, MerchantCheckout should NOT appear here. - var merchantEntries = history.FilterByWorkflowName(nameof(MerchantCheckoutWorkflow)); - Console.WriteLine($" [SettlementWorkflow] MerchantCheckout in history (expected 0): {merchantEntries.Entries.Count}"); + if (!ctx.IsReplaying) + { + Console.WriteLine($" [DispenseMedicationWorkflow] Propagated segments: {history.Entries.Count}"); + foreach (var entry in history.Entries) + { + Console.WriteLine($" [DispenseMedicationWorkflow] workflow: name={entry.WorkflowName} app={entry.AppId} events={entry.Events.Count}"); + foreach (var evt in entry.Events.Take(5)) + Console.WriteLine($" [DispenseMedicationWorkflow] event: kind={evt.Kind} id={evt.EventId}"); + if (entry.Events.Count > 5) + Console.WriteLine($" [DispenseMedicationWorkflow] ... ({entry.Events.Count - 5} more events)"); + } + + // With OwnHistory, PatientIntake should NOT appear here. + var intakeEntries = history.FilterByWorkflowName(nameof(PatientIntakeWorkflow)); + Console.WriteLine($" [DispenseMedicationWorkflow] PatientIntake in history (expected 0): {intakeEntries.Entries.Count}"); + } } - else + else if (!ctx.IsReplaying) { - Console.WriteLine(" [SettlementWorkflow] No propagated history received"); + Console.WriteLine(" [DispenseMedicationWorkflow] No propagated history received"); } - var result = await ctx.CallActivityAsync( - nameof(SettlePaymentActivity), - req); + var result = await ctx.CallActivityAsync( + nameof(DispenseMedicationActivity), + rec); - Console.WriteLine($" [SettlementWorkflow] SETTLED: {result.TransactionId}"); return result with { EventCount = eventCount }; } }