-
Notifications
You must be signed in to change notification settings - Fork 547
Add workflow context propagation sample #1327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alicejgibbons
merged 4 commits into
dapr:release-1.18
from
atrauzzi:sdk-context-propagation-update
Jun 10, 2026
Merged
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
482 changes: 482 additions & 0 deletions
482
tutorials/workflow/csharp/history-propagation/.gitignore
Large diffs are not rendered by default.
Oops, something went wrong.
212 changes: 212 additions & 0 deletions
212
tutorials/workflow/csharp/history-propagation/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| # Dapr Workflow History Propagation — Patient Intake (.NET SDK) | ||
|
|
||
| This quickstart demonstrates **workflow history propagation**, a Dapr 1.18 | ||
| feature that lets a workflow propagate its execution history to child workflows | ||
| so downstream consumers can inspect the full (or partial) execution context of | ||
| their caller — without any external state store or custom messaging. | ||
|
|
||
| The scenario is a patient intake / e-prescribing pipeline: a compliance audit | ||
| and a pharmacy dispense step refuse to act unless they can see proof — in the | ||
| propagated history — that the required upstream checks (insurance, allergies, | ||
| drug interactions) actually ran. | ||
|
|
||
| > **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) | ||
|
|
||
| ## Workflow architecture | ||
|
|
||
| ``` | ||
| PatientIntake (workflow) | ||
| ├── VerifyInsurance (activity, no propagation) | ||
| └── PrescribeMedication (child workflow, Lineage) | ||
| ├── CheckAllergies (activity, no propagation) | ||
| ├── ScreenDrugInteractions (activity, no propagation) | ||
| ├── ComplianceAudit (child workflow, Lineage) | ||
| │ → sees PatientIntake + PrescribeMedication events | ||
| └── DispenseMedicationWorkflow (child workflow, OwnHistory) | ||
| → sees PrescribeMedication events only | ||
| → refuses to dispense if the screening lineage is missing | ||
| ``` | ||
|
|
||
| ### Propagation scope | ||
|
|
||
| | Mode | Enum value | What it sends | Use case | | ||
| |------|-----------|---------------|----------| | ||
| | **Lineage** | `HistoryPropagationScope.Lineage` | Caller's own events + any ancestor events it received | Full chain-of-custody verification (compliance audits) | | ||
| | **Own history** | `HistoryPropagationScope.OwnHistory` | Caller's own events only (no ancestor chain) | Trust boundary — downstream only sees the immediate caller (pharmacy dispense) | | ||
|
|
||
| ### Key demonstration | ||
|
|
||
| - **ComplianceAudit** receives the full lineage via `HistoryPropagationScope.Lineage` — | ||
| it verifies that `VerifyInsurance` ran in the grandparent workflow | ||
| (PatientIntake), plus `CheckAllergies` and `ScreenDrugInteractions` ran in | ||
| PrescribeMedication. | ||
|
|
||
| - **DispenseMedicationWorkflow** receives only PrescribeMedication's history via | ||
| `HistoryPropagationScope.OwnHistory`. The PatientIntake ancestral history is | ||
| excluded — the pharmacy system doesn't need (or get to see) the upstream | ||
| chain. Before dispensing, the pharmacy verifies that `CheckAllergies` and | ||
| `ScreenDrugInteractions` completed in the propagated history. | ||
|
|
||
| ### Scenarios | ||
|
|
||
| The demo runs two scenarios back-to-back to show both the happy path and the | ||
| pharmacy's safety check: | ||
|
|
||
| 1. **Lineage forwarded → pharmacy dispenses.** `PrescribeMedication` calls the | ||
| dispense step with `HistoryPropagationScope.OwnHistory`. The pharmacy sees | ||
| the completed allergy and interaction screens in the propagated history and | ||
| fills the prescription. | ||
|
|
||
| 2. **Lineage withheld → pharmacy refuses.** `PrescribeMedication` calls the | ||
| dispense step **without** history propagation (simulating an upstream system | ||
| that fails to forward its lineage). With no propagated history to prove the | ||
| prescription was screened, the pharmacy refuses to dispense and returns a | ||
| `refused` result explaining what was missing. | ||
|
|
||
| ## .NET note: dispense is a child workflow | ||
|
atrauzzi marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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) which inspects the | ||
| propagated history and only calls the activity once it has verified the | ||
| screening lineage. | ||
|
|
||
| ## .NET API surface | ||
|
|
||
| ```csharp | ||
| // Parent workflow — propagate Lineage when calling a child workflow | ||
| var result = await ctx.CallChildWorkflowAsync<T>( | ||
| nameof(ComplianceAuditWorkflow), | ||
| input, | ||
| new ChildWorkflowTaskOptions(PropagationScope: HistoryPropagationScope.Lineage)); | ||
|
|
||
| // Parent workflow — propagate OwnHistory when calling a child workflow | ||
| var dispense = await ctx.CallChildWorkflowAsync<T>( | ||
| nameof(DispenseMedicationWorkflow), | ||
| 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 prescribeEntries = history.FilterByWorkflowName(nameof(PrescribeMedicationWorkflow)); | ||
|
|
||
| // Inspect events within that ancestor's segment | ||
| var completedCount = prescribeEntries.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. | ||
|
|
||
| > **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. | ||
|
|
||
| ## Running this example | ||
|
|
||
| Requires Dapr `1.18.0+` (workflow history propagation), | ||
|
atrauzzi marked this conversation as resolved.
Outdated
|
||
| `Dapr.Workflow 1.18.0-rc01+`, and the [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) | ||
|
atrauzzi marked this conversation as resolved.
Outdated
|
||
| (or newer). Redis is started automatically by `dapr init`. | ||
|
|
||
| Build the example: | ||
|
|
||
| ```bash | ||
| dotnet build ./order-processor | ||
| ``` | ||
|
|
||
| Run the demo: | ||
|
|
||
| <!-- STEP | ||
| name: Run history-propagation demo | ||
| expected_stdout_lines: | ||
| - "SCENARIO 1: lineage forwarded" | ||
| - "[ComplianceAudit] APPROVED" | ||
| - "[DispenseMedication] DISPENSED" | ||
| - "SCENARIO 2: lineage withheld" | ||
| - "[DispenseMedication] REFUSED" | ||
| - "pharmacy refused to dispense" | ||
| - "missing lineage: no propagated history received from prescriber" | ||
| output_match_mode: substring | ||
| background: false | ||
| timeout_seconds: 180 | ||
| sleep: 15 | ||
| --> | ||
|
|
||
| ```bash | ||
| dapr run -f . | ||
| ``` | ||
|
|
||
| <!-- END_STEP --> | ||
|
|
||
| The app runs both scenarios once and exits on its own — no Ctrl+C needed. | ||
|
|
||
| In scenario 1 (lineage forwarded) you'll see the pharmacy dispense: | ||
|
|
||
| ``` | ||
| [ComplianceAudit] Received propagated history with 2 segment(s): | ||
| [ComplianceAudit] APPROVED (risk=0.10, total events inspected=...) | ||
| [DispenseMedication] Dispense request: amoxicillin 500mg for P-1042 (propagated history: ... events) | ||
| [DispenseMedication] DISPENSED: rx-P-1042-... | ||
| ``` | ||
|
|
||
| In scenario 2 (lineage withheld) the pharmacy refuses: | ||
|
|
||
| ``` | ||
| [PrescribeMedication] Step 4: DispenseMedicationWorkflow child wf | ||
| -> NO history propagation (negative scenario) | ||
| [DispenseMedication] Dispense request: penicillin 500mg for P-2087 (propagated history: none) | ||
| [DispenseMedication] REFUSED — no propagated history; cannot verify screening for P-2087 | ||
| [PrescribeMedication] Step 4 BLOCKED: pharmacy refused to dispense (missing lineage: no propagated history received from prescriber) | ||
| ``` | ||
|
|
||
| ## 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. | ||
|
|
||
| ## Files | ||
|
atrauzzi marked this conversation as resolved.
Outdated
|
||
|
|
||
| ``` | ||
| sdk-context-propagation/ | ||
| ├── README.md # this file | ||
| ├── dapr.yaml # `dapr run -f .` config (appID, resources, command) | ||
| └── order-processor/ | ||
| ├── Program.cs # host setup; schedules both scenarios | ||
| ├── Models.cs # PatientRecord, ComplianceResult, DispenseResult | ||
| ├── Activities.cs # VerifyInsurance, CheckAllergies, ScreenDrugInteractions, DispenseMedication | ||
| ├── Workflows.cs # workflow definitions + history inspection | ||
| └── OrderProcessor.csproj # project + Dapr.Workflow dependency | ||
| ``` | ||
|
|
||
| ## References | ||
|
atrauzzi marked this conversation as resolved.
Outdated
|
||
|
|
||
| - 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) | ||
| - Proposal: [dapr/proposals#102](https://github.com/dapr/proposals/issues/102) | ||
| - 1.18 endgame: [dapr/dapr#9856](https://github.com/dapr/dapr/issues/9856) | ||
| ``` | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| version: 1 | ||
| common: | ||
| resourcesPath: ../../components | ||
| apps: | ||
| - appID: order-processor | ||
|
atrauzzi marked this conversation as resolved.
Outdated
|
||
| appDirPath: ./order-processor/ | ||
| command: ["dotnet", "run"] | ||
72 changes: 72 additions & 0 deletions
72
tutorials/workflow/csharp/history-propagation/order-processor/Activities.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| // ------------------------------------------------------------------------ | ||
| // 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; | ||
|
|
||
| /// <summary> | ||
| /// Verifies the patient's insurance coverage. Called by PatientIntake without | ||
| /// propagation. | ||
| /// </summary> | ||
| public sealed class VerifyInsuranceActivity : WorkflowActivity<PatientRecord, bool> | ||
| { | ||
| public override Task<bool> RunAsync(WorkflowActivityContext ctx, PatientRecord rec) | ||
| { | ||
| Console.WriteLine($" [VerifyInsurance] Checking coverage for patient {rec.PatientId}"); | ||
| return Task.FromResult(true); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Screens the patient against their allergy list for the candidate drug. | ||
| /// Called by PrescribeMedication without propagation. | ||
| /// </summary> | ||
| public sealed class CheckAllergiesActivity : WorkflowActivity<PatientRecord, bool> | ||
| { | ||
| public override Task<bool> RunAsync(WorkflowActivityContext ctx, PatientRecord rec) | ||
| { | ||
| Console.WriteLine($" [CheckAllergies] Screening {rec.PatientId} for {rec.Medication}"); | ||
| return Task.FromResult(true); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Screens the candidate prescription against the patient's active medication | ||
| /// list. Called by PrescribeMedication without propagation. | ||
| /// </summary> | ||
| public sealed class ScreenDrugInteractionsActivity : WorkflowActivity<PatientRecord, bool> | ||
| { | ||
| public override Task<bool> RunAsync(WorkflowActivityContext ctx, PatientRecord rec) | ||
| { | ||
| Console.WriteLine($" [ScreenDrugInteractions] Screening {rec.Medication} {rec.Dosage:F0}mg for {rec.PatientId}"); | ||
| return Task.FromResult(true); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Fills the prescription. Called by DispenseMedicationWorkflow only after it | ||
| /// has verified the prescribing pipeline in the propagated history. | ||
| /// </summary> | ||
| public sealed class DispenseMedicationActivity : WorkflowActivity<PatientRecord, DispenseResult> | ||
| { | ||
| public override Task<DispenseResult> RunAsync(WorkflowActivityContext ctx, PatientRecord rec) | ||
| { | ||
| var dispenseId = $"rx-{rec.PatientId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; | ||
| Console.WriteLine($" [DispenseMedication] DISPENSED: {dispenseId}"); | ||
| return Task.FromResult(new DispenseResult( | ||
| DispenseId: dispenseId, | ||
| Status: "dispensed", | ||
| EventCount: 0)); // EventCount populated by DispenseMedicationWorkflow | ||
| } | ||
| } |
67 changes: 67 additions & 0 deletions
67
tutorials/workflow/csharp/history-propagation/order-processor/Models.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| // ------------------------------------------------------------------------ | ||
| // 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; | ||
|
|
||
| /// <summary> | ||
| /// 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. | ||
| /// </summary> | ||
| /// <param name="PatientId">Patient identifier.</param> | ||
| /// <param name="Name">Patient name.</param> | ||
| /// <param name="Dob">Date of birth (YYYY-MM-DD).</param> | ||
| /// <param name="Mrn">Medical record number.</param> | ||
| /// <param name="Condition">Diagnosis / indication.</param> | ||
| /// <param name="Medication">Prescribed drug name.</param> | ||
| /// <param name="Dosage">Dosage in milligrams.</param> | ||
| /// <param name="ForwardLineage"> | ||
| /// Controls whether PrescribeMedication propagates its own history to the | ||
| /// dispense step. When <c>true</c> (happy path) the pharmacy can verify the | ||
| /// upstream screening and dispenses. When <c>false</c> (negative scenario) | ||
| /// the pharmacy receives no lineage and refuses. | ||
| /// </param> | ||
| public sealed record PatientRecord( | ||
| string PatientId, | ||
| string Name, | ||
| string Dob, | ||
| string Mrn, | ||
| string Condition, | ||
| string Medication, | ||
| double Dosage, | ||
| bool ForwardLineage = true); | ||
|
|
||
| /// <summary>Result produced by the ComplianceAudit workflow.</summary> | ||
| /// <param name="Compliant">Whether the prescription cleared compliance.</param> | ||
| /// <param name="RiskScore">Risk score in the range [0, 1].</param> | ||
| /// <param name="Reason">Human-readable decision rationale.</param> | ||
| /// <param name="EventCount">Number of propagated history segments inspected.</param> | ||
| public sealed record ComplianceResult( | ||
| bool Compliant, | ||
| double RiskScore, | ||
| string Reason, | ||
| int EventCount); | ||
|
|
||
| /// <summary>Result produced by the dispense step.</summary> | ||
|
atrauzzi marked this conversation as resolved.
Outdated
|
||
| /// <param name="DispenseId">Pharmacy dispense identifier (empty when refused).</param> | ||
| /// <param name="Status"><c>"dispensed"</c> when the pharmacy filled the | ||
| /// prescription, or <c>"refused"</c> when it could not verify the prescribing | ||
| /// pipeline in the propagated history.</param> | ||
| /// <param name="EventCount">Number of propagated history events inspected.</param> | ||
| /// <param name="Reason">Explains what was missing when <see cref="Status"/> is | ||
| /// <c>"refused"</c>; empty otherwise.</param> | ||
| public sealed record DispenseResult( | ||
| string DispenseId, | ||
| string Status, | ||
| int EventCount, | ||
| string Reason = ""); | ||
17 changes: 17 additions & 0 deletions
17
tutorials/workflow/csharp/history-propagation/order-processor/OrderProcessor.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <OutputType>Exe</OutputType> | ||
| <Nullable>enable</Nullable> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <LangVersion>latest</LangVersion> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Dapr.Workflow" Version="1.18.0-rc01" /> | ||
| <PackageReference Include="Dapr.AspNetCore" Version="1.18.0-rc01" /> | ||
| <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.3" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.