Repository: Azure/azure-functions-durable-js
Pull request #640 (2025-06-21 06:24:37)

Add version property to DurableOrchestrationContext class
Add 'version' property to the `context.df` object passed to orchestrator functions.

As a result, Node.js Functions users will be able to specify a version in **host.json**:

``` json
{
  "extensions": {
    "durableTask": {
      "defaultVersion": "2.0"
    }
  }
}
```

and check the orchestration version in their orchestrator functions in order to keep them backward compatible, for example:

``` javascript
if (context.df.version === "1.0") {
    // Legacy code path
    yield context.df.callActivity('A');
} else {
    // New code path
    yield context.df.callActivity('B');
}
```

**Note: Microsoft.Azure.WebJobs.Extensions.DurableTask 3.1.0+ is required for this to work properly.**


Patch:
diff --git a/samples-ts/functions/orchestrationVersion.ts b/samples-ts/functions/orchestrationVersion.ts
new file mode 100644
index 0000000..737bf6c
--- /dev/null
+++ b/samples-ts/functions/orchestrationVersion.ts
@@ -0,0 +1,45 @@
+import * as df from "durable-functions";
+import { ActivityHandler, OrchestrationContext, OrchestrationHandler } from "durable-functions";
+
+const versionedOrchestrator: OrchestrationHandler = function* (context: OrchestrationContext) {
+    // context.df.version contains the value of defaultVersion in host.json
+    // at the moment when the orchestration was created.
+    let activityResult;
+    if (context.df.version === "1.0") {
+        // Legacy code path
+        activityResult = yield context.df.callActivity("ActivityA");
+    } else {
+        // New code path
+        activityResult = yield context.df.callActivity("ActivityB");
+    }
+
+    // Provide an opportunity to update and restart the app
+    context.df.setCustomStatus("Waiting for Continue event...");
+    yield context.df.waitForExternalEvent("Continue");
+    context.df.setCustomStatus("Continue event received");
+
+    // New sub-orchestrations will use the current defaultVersion specified in host.json
+    const subOrchestratorResult = yield context.df.callSubOrchestrator("versionedSuborchestrator");
+
+    return [
+        `Orchestration version: ${context.df.version}`,
+        `Suborchestration version: ${subOrchestratorResult}`,
+        `Activity result: ${activityResult}`,
+    ];
+};
+df.app.orchestration("versionedOrchestrator", versionedOrchestrator);
+
+const versionedSuborchestrator: OrchestrationHandler = function* (context: OrchestrationContext) {
+    return context.df.version;
+};
+df.app.orchestration("versionedSuborchestrator", versionedSuborchestrator);
+
+const ActivityA: ActivityHandler = (): string => {
+    return `Hello from A`;
+};
+df.app.activity("ActivityA", { handler: ActivityA });
+
+const ActivityB: ActivityHandler = (): string => {
+    return `Hello from B`;
+};
+df.app.activity("ActivityB", { handler: ActivityB });
diff --git a/samples-ts/host.json b/samples-ts/host.json
index 23647b9..5753410 100644
--- a/samples-ts/host.json
+++ b/samples-ts/host.json
@@ -11,5 +11,10 @@
     "extensionBundle": {
         "id": "Microsoft.Azure.Functions.ExtensionBundle",
         "version": "[3.15.0, 4.0.0)"
+    },
+    "extensions": {
+        "durableTask": {
+            "defaultVersion": "1.0"
+        }
     }
 }
diff --git a/src/history/ExecutionStartedEvent.ts b/src/history/ExecutionStartedEvent.ts
index 45f0c00..31d73e2 100644
--- a/src/history/ExecutionStartedEvent.ts
+++ b/src/history/ExecutionStartedEvent.ts
@@ -6,6 +6,7 @@ import { HistoryEventType } from "./HistoryEventType";
 export class ExecutionStartedEvent extends HistoryEvent {
     public Name: string;
     public Input: string | undefined;
+    public Version: string | undefined;
 
     constructor(options: HistoryEventOptions) {
         super(
@@ -22,5 +23,6 @@ export class ExecutionStartedEvent extends HistoryEvent {
         }
 
         this.Input = options.input;
+        this.Version = options.version;
     }
 }
diff --git a/src/history/HistoryEventOptions.ts b/src/history/HistoryEventOptions.ts
index 36aabac..332c76f 100644
--- a/src/history/HistoryEventOptions.ts
+++ b/src/history/HistoryEventOptions.ts
@@ -9,6 +9,7 @@ export class HistoryEventOptions {
     public result?: string;
     public taskScheduledId?: number;
     public timerId?: number;
+    public version?: string;
 
     constructor(public eventId: number, public timestamp: Date, public isPlayed: boolean = false) {}
 }
diff --git a/src/orchestrations/DurableOrchestrationContext.ts b/src/orchestrations/DurableOrchestrationContext.ts
index 6043b8b..65b09e0 100644
--- a/src/orchestrations/DurableOrchestrationContext.ts
+++ b/src/orchestrations/DurableOrchestrationContext.ts
@@ -29,6 +29,8 @@ import { WaitForExternalEventAction } from "../actions/WaitForExternalEventActio
 import { GuidManager } from "../util/GuidManager";
 import { HistoryEvent } from "../history/HistoryEvent";
 import { DurableHttpRequest } from "../http/DurableHttpRequest";
+import { HistoryEventType } from "../history/HistoryEventType";
+import { ExecutionStartedEvent } from "../history/ExecutionStartedEvent";
 
 /**
  * Parameter data for orchestration bindings that can be used to schedule
@@ -63,12 +65,14 @@ export class DurableOrchestrationContext implements types.DurableOrchestrationCo
         this.schemaVersion = schemaVersion;
         this.input = input;
         this.newGuidCounter = 0;
+        this.version = this.extractVersionFromHistory(state);
     }
 
     private input: unknown;
     private readonly state: HistoryEvent[];
     private newGuidCounter: number;
     public customStatus: unknown;
+    public readonly version: string | undefined;
 
     /**
      * The default time to wait between attempts when making HTTP polling requests
@@ -121,6 +125,19 @@ export class DurableOrchestrationContext implements types.DurableOrchestrationCo
         return tasks.every((x) => x instanceof DFTask);
     }
 
+    /**
+     * Extracts the version value from the ExecutionStarted event in the history
+     * @param state The history events array
+     * @returns The version string from ExecutionStartedEvent, or undefined if not found
+     */
+    private extractVersionFromHistory(state: HistoryEvent[]): string | undefined {
+        const executionStartedEvent = state.find(
+            (e) => e.EventType === HistoryEventType.ExecutionStarted
+        ) as ExecutionStartedEvent | undefined;
+
+        return executionStartedEvent?.Version;
+    }
+
     public Task = {
         all: (tasks: Task[]): Task => {
             if (this.isDFTaskArray(tasks)) {
diff --git a/types/orchestration.d.ts b/types/orchestration.d.ts
index 611512f..04e71e7 100644
--- a/types/orchestration.d.ts
+++ b/types/orchestration.d.ts
@@ -84,6 +84,11 @@ export declare class DurableOrchestrationContext {
      */
     readonly parentInstanceId: string | undefined;
 
+    /**
+     * The version assigned to the orchestration instance on creation.
+     */
+    readonly version: string | undefined;
+
     /**
      * Gets a value indicating whether the orchestrator function is currently
      * replaying itself.


Test patch:
diff --git a/test/integration/orchestrator-spec.ts b/test/integration/orchestrator-spec.ts
index a1336dc..c94d18a 100644
--- a/test/integration/orchestrator-spec.ts
+++ b/test/integration/orchestrator-spec.ts
@@ -212,6 +212,34 @@ describe("Orchestrator", () => {
             expect(mockContext.df!.parentInstanceId).to.be.equal(id);
         });
 
+        it("extracts version from history", async () => {
+            const orchestrator = TestOrchestrations.SayHelloSequence;
+            const name = "World";
+            const version = "1.0.0";
+
+            const mockContext = new DummyOrchestrationContext();
+            const orchestrationInput = new DurableOrchestrationInput(
+                "",
+                TestHistories.GetSayHelloWithActivityReplayOne(
+                    "SayHelloWithActivity",
+                    moment.utc().toDate(),
+                    name,
+                    version
+                ),
+                name,
+                undefined,
+                undefined,
+                undefined,
+                ReplaySchema.V1,
+                undefined,
+                undefined
+            );
+
+            await orchestrator(orchestrationInput, mockContext);
+
+            expect(mockContext.df!.version).to.be.equal(version);
+        });
+
         it("updates currentUtcDateTime to the most recent OrchestratorStarted timestamp", async () => {
             const orchestrator = TestOrchestrations.SayHelloSequence;
             const name = "World";
diff --git a/test/testobjects/testhistories.ts b/test/testobjects/testhistories.ts
index c3fae05..8edd0c1 100644
--- a/test/testobjects/testhistories.ts
+++ b/test/testobjects/testhistories.ts
@@ -1130,7 +1130,8 @@ export class TestHistories {
     public static GetSayHelloWithActivityReplayOne(
         name: string,
         firstTimestamp: Date,
-        input: unknown
+        input: unknown,
+        version?: string
     ): HistoryEvent[] {
         return [
             new OrchestratorStartedEvent({
@@ -1144,6 +1145,7 @@ export class TestHistories {
                 isPlayed: true,
                 name,
                 input: JSON.stringify(input),
+                version: version,
             }),
             new TaskScheduledEvent({
                 eventId: 0,