diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln
index 53f5f4bcf..5b0b1332b 100644
--- a/Microsoft.DurableTask.sln
+++ b/Microsoft.DurableTask.sln
@@ -101,6 +101,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost", "src\In
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost.Tests", "test\InProcessTestHost.Tests\InProcessTestHost.Tests.csproj", "{B894780C-338F-475E-8E84-56AFA8197A06}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportHistory", "src\ExportHistory\ExportHistory.csproj", "{354CE69B-78DB-9B29-C67E-0DBB862C7A65}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportHistoryWebApp", "samples\ExportHistoryWebApp\ExportHistoryWebApp.csproj", "{FE1E17DD-595A-123A-EA4C-AA313BBFB685}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -255,14 +259,6 @@ Global
{D2779F32-A548-44F8-B60A-6AC018966C79}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.Build.0 = Release|Any CPU
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.Build.0 = Release|Any CPU
- {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.Build.0 = Release|Any CPU
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -271,6 +267,22 @@ Global
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.Build.0 = Release|Any CPU
+ {354CE69B-78DB-9B29-C67E-0DBB862C7A65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {354CE69B-78DB-9B29-C67E-0DBB862C7A65}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {354CE69B-78DB-9B29-C67E-0DBB862C7A65}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {354CE69B-78DB-9B29-C67E-0DBB862C7A65}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FE1E17DD-595A-123A-EA4C-AA313BBFB685}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FE1E17DD-595A-123A-EA4C-AA313BBFB685}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FE1E17DD-595A-123A-EA4C-AA313BBFB685}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FE1E17DD-595A-123A-EA4C-AA313BBFB685}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -317,10 +329,12 @@ Global
{A89B766C-987F-4C9F-8937-D0AB9FE640C8} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{100348B5-4D97-4A3F-B777-AB14F276F8FE} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{D2779F32-A548-44F8-B60A-6AC018966C79} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
- {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
- {B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
+ {B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
+ {354CE69B-78DB-9B29-C67E-0DBB862C7A65} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
+ {FE1E17DD-595A-123A-EA4C-AA313BBFB685} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71}
diff --git a/samples/ExportHistoryWebApp/ExportHistoryWebApp.csproj b/samples/ExportHistoryWebApp/ExportHistoryWebApp.csproj
new file mode 100644
index 000000000..884da7e8b
--- /dev/null
+++ b/samples/ExportHistoryWebApp/ExportHistoryWebApp.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net6.0
+ enable
+ enable
+ true
+ $(BaseIntermediateOutputPath)Generated
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ExportHistoryWebApp/ExportHistoryWebApp.http b/samples/ExportHistoryWebApp/ExportHistoryWebApp.http
new file mode 100644
index 000000000..aeea8e8c0
--- /dev/null
+++ b/samples/ExportHistoryWebApp/ExportHistoryWebApp.http
@@ -0,0 +1,89 @@
+### Variables
+@baseUrl = http://localhost:5010
+@jobId = export-job-12345
+
+### Create a new batch export job
+# @name createBatchExportJob
+POST {{baseUrl}}/export-jobs
+Content-Type: application/json
+
+{
+ "jobId": "{{jobId}}",
+ "mode": "Batch",
+ "completedTimeFrom": "2025-10-01T00:00:00Z",
+ "completedTimeTo": "2025-11-06T23:59:59Z",
+ "container": "export-history",
+ # "prefix": "exports/",
+ "maxInstancesPerBatch": 1,
+ "runtimeStatus": []
+}
+
+### Create a new continuous export job
+# @name createContinuousExportJob
+POST {{baseUrl}}/export-jobs
+Content-Type: application/json
+
+{
+ "jobId": "export-job-continuous-123",
+ "mode": "Continuous",
+ "container": "export-history",
+ # "prefix": "continuous-exports/",
+ "maxInstancesPerBatch": 1000
+ # "runtimeStatus": ["asdasd"]
+}
+
+### Create an export job with default storage (no container specified)
+# @name createExportJobWithDefaultStorage
+POST {{baseUrl}}/export-jobs
+Content-Type: application/json
+{
+ "jobId": "export-job-default-storage",
+ "mode": "Batch",
+ "completedTimeFrom": "2024-01-01T00:00:00Z",
+ "completedTimeTo": "2024-12-31T23:59:59Z",
+ "maxInstancesPerBatch": 100
+}
+
+### Get a specific export job by ID
+# Note: This endpoint can be used to verify the export job was created and check its status
+# The ID in the URL should match the jobId used in create request
+GET {{baseUrl}}/export-jobs/{{jobId}}
+
+### List all export jobs
+GET {{baseUrl}}/export-jobs/list
+
+### List export jobs with filters
+### Filter by status
+GET {{baseUrl}}/export-jobs/list?status=Active
+
+### Filter by job ID prefix
+GET {{baseUrl}}/export-jobs/list?jobIdPrefix=export-job-
+
+### Filter by creation time range
+GET {{baseUrl}}/export-jobs/list?createdFrom=2024-01-01T00:00:00Z&createdTo=2024-12-31T23:59:59Z
+
+### Combined filters
+GET {{baseUrl}}/export-jobs/list?status=Completed&jobIdPrefix=export-job-&pageSize=50
+
+### Delete an export job
+# DELETE {{baseUrl}}/export-jobs/{{jobId}}
+
+# Delete a continuous export job
+DELETE {{baseUrl}}/export-jobs/export-job-continuous-123jk
+
+### Tips:
+# - Replace the baseUrl variable if your application runs on a different port
+# - The jobId variable can be changed to test different export job instances
+# - Export modes:
+# - "Batch": Exports all instances within a time range (requires completedTimeTo)
+# - "Continuous": Continuously exports instances from a start time (completedTimeTo must be null)
+# - Runtime status filters (valid values):
+# - "Completed": Exports only completed orchestrations
+# - "Failed": Exports only failed orchestrations
+# - "Terminated": Exports only terminated orchestrations
+# - "ContinuedAsNew": Exports only continued-as-new orchestrations
+# - Dates are in ISO 8601 format (YYYY-MM-DDThh:mm:ssZ)
+# - You can use the REST Client extension in VS Code to execute these requests
+# - The @name directive allows referencing the response in subsequent requests
+# - Export jobs run asynchronously; use GET to check the status after creation
+
diff --git a/samples/ExportHistoryWebApp/ExportJobController.cs b/samples/ExportHistoryWebApp/ExportJobController.cs
new file mode 100644
index 000000000..859950429
--- /dev/null
+++ b/samples/ExportHistoryWebApp/ExportJobController.cs
@@ -0,0 +1,202 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.ExportHistory;
+using ExportHistoryWebApp.Models;
+
+namespace ExportHistoryWebApp.Controllers;
+
+///
+/// Controller for managing export history jobs through a REST API.
+/// Provides endpoints for creating, reading, listing, and deleting export jobs.
+///
+[ApiController]
+[Route("export-jobs")]
+public class ExportJobController : ControllerBase
+{
+ readonly ExportHistoryClient exportHistoryClient;
+ readonly ILogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Client for managing export history jobs.
+ /// Logger for recording controller operations.
+ public ExportJobController(
+ ExportHistoryClient exportHistoryClient,
+ ILogger logger)
+ {
+ this.exportHistoryClient = exportHistoryClient ?? throw new ArgumentNullException(nameof(exportHistoryClient));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// Creates a new export job based on the provided configuration.
+ ///
+ /// The export job creation request.
+ /// The created export job description.
+ [HttpPost]
+ public async Task> CreateExportJob([FromBody] CreateExportJobRequest request)
+ {
+ if (request == null)
+ {
+ return this.BadRequest("createExportJobRequest cannot be null");
+ }
+
+ try
+ {
+ ExportDestination? destination = null;
+ if (!string.IsNullOrEmpty(request.Container))
+ {
+ destination = new ExportDestination(request.Container)
+ {
+ Prefix = request.Prefix,
+ };
+ }
+
+ ExportJobCreationOptions creationOptions = new ExportJobCreationOptions(
+ mode: request.Mode,
+ completedTimeFrom: request.CompletedTimeFrom,
+ completedTimeTo: request.CompletedTimeTo,
+ destination: destination,
+ jobId: request.JobId,
+ format: request.Format,
+ runtimeStatus: request.RuntimeStatus,
+ maxInstancesPerBatch: request.MaxInstancesPerBatch);
+
+ ExportHistoryJobClient jobClient = await this.exportHistoryClient.CreateJobAsync(creationOptions);
+ ExportJobDescription description = await jobClient.DescribeAsync();
+
+ this.logger.LogInformation("Created new export job with ID: {JobId}", description.JobId);
+
+ return this.CreatedAtAction(nameof(GetExportJob), new { id = description.JobId }, description);
+ }
+ catch (ArgumentException ex)
+ {
+ this.logger.LogError(ex, "Validation failed while creating export job {JobId}", request.JobId);
+ return this.BadRequest(ex.Message);
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error creating export job {JobId}", request.JobId);
+ return this.StatusCode(500, "An error occurred while creating the export job");
+ }
+ }
+
+ ///
+ /// Retrieves a specific export job by its ID.
+ ///
+ /// The ID of the export job to retrieve.
+ /// The export job description if found.
+ [HttpGet("{id}")]
+ public async Task> GetExportJob(string id)
+ {
+ try
+ {
+ ExportJobDescription? job = await this.exportHistoryClient.GetJobAsync(id);
+ return this.Ok(job);
+ }
+ catch (ExportJobNotFoundException)
+ {
+ return this.NotFound();
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error retrieving export job {JobId}", id);
+ return this.StatusCode(500, "An error occurred while retrieving the export job");
+ }
+ }
+
+ ///
+ /// Lists all export jobs, optionally filtered by query parameters.
+ ///
+ /// Optional filter by job status.
+ /// Optional filter by job ID prefix.
+ /// Optional filter for jobs created after this time.
+ /// Optional filter for jobs created before this time.
+ /// Optional page size for pagination.
+ /// Optional continuation token for pagination.
+ /// A collection of export job descriptions.
+ [HttpGet("list")]
+ public async Task>> ListExportJobs(
+ [FromQuery] ExportJobStatus? status = null,
+ [FromQuery] string? jobIdPrefix = null,
+ [FromQuery] DateTimeOffset? createdFrom = null,
+ [FromQuery] DateTimeOffset? createdTo = null,
+ [FromQuery] int? pageSize = null,
+ [FromQuery] string? continuationToken = null)
+ {
+ this.logger.LogInformation("GET list endpoint called with method: {Method}", this.HttpContext.Request.Method);
+ try
+ {
+ ExportJobQuery? query = null;
+ if (
+ status.HasValue ||
+ !string.IsNullOrEmpty(jobIdPrefix) ||
+ createdFrom.HasValue ||
+ createdTo.HasValue ||
+ pageSize.HasValue ||
+ !string.IsNullOrEmpty(continuationToken)
+ )
+ {
+ query = new ExportJobQuery
+ {
+ Status = status,
+ JobIdPrefix = jobIdPrefix,
+ CreatedFrom = createdFrom,
+ CreatedTo = createdTo,
+ PageSize = pageSize,
+ ContinuationToken = continuationToken,
+ };
+ }
+
+ AsyncPageable jobs = this.exportHistoryClient.ListJobsAsync(query);
+
+ // Collect all jobs from the async pageable
+ List jobList = new List();
+ await foreach (ExportJobDescription job in jobs)
+ {
+ jobList.Add(job);
+ }
+
+ return this.Ok(jobList);
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error retrieving export jobs");
+ return this.StatusCode(500, "An error occurred while retrieving export jobs");
+ }
+ }
+
+ ///
+ /// Deletes an export job by its ID.
+ ///
+ /// The ID of the export job to delete.
+ /// No content if successful.
+ [HttpDelete("{id}")]
+ public async Task DeleteExportJob(string id)
+ {
+ this.logger.LogInformation("DELETE endpoint called for job ID: {JobId}", id);
+ try
+ {
+ ExportHistoryJobClient jobClient = this.exportHistoryClient.GetJobClient(id);
+ await jobClient.DeleteAsync();
+ this.logger.LogInformation("Successfully deleted export job {JobId}", id);
+ return this.NoContent();
+ }
+ catch (ExportJobNotFoundException)
+ {
+ this.logger.LogWarning("Export job {JobId} not found for deletion", id);
+ return this.NotFound();
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Error deleting export job {JobId}", id);
+ return this.StatusCode(500, "An error occurred while deleting the export job");
+ }
+ }
+}
+
diff --git a/samples/ExportHistoryWebApp/Models/CreateExportJobRequest.cs b/samples/ExportHistoryWebApp/Models/CreateExportJobRequest.cs
new file mode 100644
index 000000000..1e09ddf51
--- /dev/null
+++ b/samples/ExportHistoryWebApp/Models/CreateExportJobRequest.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.ExportHistory;
+
+namespace ExportHistoryWebApp.Models;
+
+///
+/// Represents a request to create a new export job.
+///
+public class CreateExportJobRequest
+{
+ ///
+ /// Gets or sets the unique identifier for the export job. If not provided, a GUID will be generated.
+ ///
+ public string? JobId { get; set; }
+
+ ///
+ /// Gets or sets the export mode (Batch or Continuous).
+ ///
+ public ExportMode Mode { get; set; }
+
+ ///
+ /// Gets or sets the start time for the export based on completion time (inclusive). Required.
+ ///
+ public DateTimeOffset CompletedTimeFrom { get; set; }
+
+ ///
+ /// Gets or sets the end time for the export based on completion time (inclusive). Required for Batch mode, null for Continuous mode.
+ ///
+ public DateTimeOffset? CompletedTimeTo { get; set; }
+
+ ///
+ /// Gets or sets the blob container name where exported data will be stored. Optional if default storage is configured.
+ ///
+ public string? Container { get; set; }
+
+ ///
+ /// Gets or sets an optional prefix for blob paths.
+ ///
+ public string? Prefix { get; set; }
+
+ ///
+ /// Gets or sets the export format settings. Optional, defaults to jsonl-gzip.
+ ///
+ public ExportFormat? Format { get; set; }
+
+ ///
+ /// Gets or sets the orchestration runtime statuses to filter by. Optional.
+ /// Valid statuses are: Completed, Failed, Terminated.
+ ///
+ public List? RuntimeStatus { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of instances to fetch per batch. Optional, defaults to 100.
+ ///
+ public int? MaxInstancesPerBatch { get; set; }
+}
+
diff --git a/samples/ExportHistoryWebApp/Program.cs b/samples/ExportHistoryWebApp/Program.cs
new file mode 100644
index 000000000..46ee02f41
--- /dev/null
+++ b/samples/ExportHistoryWebApp/Program.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Text.Json.Serialization;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Client.AzureManaged;
+using Microsoft.DurableTask.ExportHistory;
+using Microsoft.DurableTask.Worker;
+using Microsoft.DurableTask.Worker.AzureManaged;
+
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
+
+string connectionString = builder.Configuration.GetValue("DURABLE_TASK_CONNECTION_STRING")
+ ?? throw new InvalidOperationException("Missing required configuration 'DURABLE_TASK_CONNECTION_STRING'");
+
+string storageConnectionString = builder.Configuration.GetValue("EXPORT_HISTORY_STORAGE_CONNECTION_STRING")
+ ?? throw new InvalidOperationException("Missing required configuration 'EXPORT_HISTORY_STORAGE_CONNECTION_STRING'");
+
+string containerName = builder.Configuration.GetValue("EXPORT_HISTORY_CONTAINER_NAME")
+ ?? throw new InvalidOperationException("Missing required configuration 'EXPORT_HISTORY_CONTAINER_NAME'");
+
+builder.Services.AddSingleton(sp => sp.GetRequiredService().CreateLogger());
+builder.Services.AddLogging();
+
+// Add Durable Task worker with export history support
+builder.Services.AddDurableTaskWorker(builder =>
+{
+ builder.UseDurableTaskScheduler(connectionString);
+ builder.UseExportHistory();
+});
+
+// Register the client with export history support
+builder.Services.AddDurableTaskClient(clientBuilder =>
+{
+ clientBuilder.UseDurableTaskScheduler(connectionString);
+ clientBuilder.UseExportHistory(options =>
+ {
+ options.ConnectionString = storageConnectionString;
+ options.ContainerName = containerName;
+ options.Prefix = builder.Configuration.GetValue("EXPORT_HISTORY_PREFIX");
+ });
+});
+
+// Configure the HTTP request pipeline
+builder.Services.AddControllers().AddJsonOptions(options =>
+{
+ options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
+ options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
+});
+
+// The actual listen URL can be configured in environment variables named "ASPNETCORE_URLS" or "ASPNETCORE_URLS_HTTPS"
+WebApplication app = builder.Build();
+app.MapControllers();
+app.Run();
+
diff --git a/samples/ExportHistoryWebApp/Properties/launchSettings.json b/samples/ExportHistoryWebApp/Properties/launchSettings.json
new file mode 100644
index 000000000..4375d42ec
--- /dev/null
+++ b/samples/ExportHistoryWebApp/Properties/launchSettings.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:47698",
+ "sslPort": 44372
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "applicationUrl": "http://localhost:5009",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DURABLE_TASK_CONNECTION_STRING": "",
+ "EXPORT_HISTORY_STORAGE_CONNECTION_STRING": "",
+ "EXPORT_HISTORY_CONTAINER_NAME": "export-history",
+ "EXPORT_HISTORY_PREFIX": ""
+ }
+ }
+ }
+}
+
diff --git a/samples/ExportHistoryWebApp/appsettings.Development.json b/samples/ExportHistoryWebApp/appsettings.Development.json
new file mode 100644
index 000000000..21b22dbb4
--- /dev/null
+++ b/samples/ExportHistoryWebApp/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
+
diff --git a/samples/ExportHistoryWebApp/appsettings.json b/samples/ExportHistoryWebApp/appsettings.json
new file mode 100644
index 000000000..fb8785032
--- /dev/null
+++ b/samples/ExportHistoryWebApp/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
+
diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs
index f2d658d8f..1ea0b2554 100644
--- a/src/Client/Core/DurableTaskClient.cs
+++ b/src/Client/Core/DurableTaskClient.cs
@@ -1,441 +1,442 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System.ComponentModel;
-using Microsoft.DurableTask.Client.Entities;
-using Microsoft.DurableTask.Internal;
-
-namespace Microsoft.DurableTask.Client;
-
-///
-/// Base class that defines client operations for managing durable task instances.
-///
-///
-///
-/// Instances of can be used to start, query, raise events to, and terminate
-/// orchestration instances. In most cases, methods on this class accept an instance ID as a parameter, which identifies
-/// the orchestration instance.
-///
-/// At the time of writing, the most common implementation of this class is the gRPC client, which works by making gRPC
-/// calls to a remote service (e.g. a sidecar) that implements the operation behavior. To ensure any owned network
-/// resources are properly released, instances of should be disposed when they are no
-/// longer needed.
-///
-/// Instances of this class are expected to be safe for multithreaded apps. You can therefore safely cache instances
-/// of this class and reuse them across multiple contexts. Caching these objects is useful to improve overall
-/// performance.
-///
-///
-public abstract class DurableTaskClient : IOrchestrationSubmitter, IAsyncDisposable
-{
- ///
- /// Initializes a new instance of the class.
- ///
- /// The name of the client.
- protected DurableTaskClient(string name)
- {
- this.Name = name;
- }
-
- ///
- /// Gets the name of the client.
- ///
- public string Name { get; }
-
- ///
- /// Gets the for interacting with durable entities.
- ///
- ///
- /// Not all clients support durable entities. Refer to a specific client implementation for verifying support.
- ///
- public virtual DurableEntityClient Entities =>
- throw new NotSupportedException($"{this.GetType()} does not support durable entities.");
-
- ///
- public virtual Task ScheduleNewOrchestrationInstanceAsync(
- TaskName orchestratorName, CancellationToken cancellation)
- => this.ScheduleNewOrchestrationInstanceAsync(orchestratorName, null, null, cancellation);
-
- ///
- public virtual Task ScheduleNewOrchestrationInstanceAsync(
- TaskName orchestratorName, object? input, CancellationToken cancellation)
- => this.ScheduleNewOrchestrationInstanceAsync(orchestratorName, input, null, cancellation);
-
- ///
- public virtual Task ScheduleNewOrchestrationInstanceAsync(
- TaskName orchestratorName, StartOrchestrationOptions options, CancellationToken cancellation = default)
- => this.ScheduleNewOrchestrationInstanceAsync(orchestratorName, null, options, cancellation);
-
- ///
- /// Schedules a new orchestration instance for execution.
- ///
- ///
- /// All orchestrations must have a unique instance ID. You can provide an instance ID using the
- /// parameter or you can omit this and a random instance ID will be
- /// generated for you automatically. If an orchestration with the specified instance ID already exists and is in a
- /// non-terminal state (Pending, Running, etc.), then this operation may fail silently. However, if an orchestration
- /// instance with this ID already exists in a terminal state (Completed, Terminated, Failed, etc.) then the instance
- /// may be recreated automatically, depending on the configuration of the backend instance store.
- ///
- /// Orchestration instances started with this method will be created in the
- /// state and will transition to the
- /// after successfully awaiting its first task. The exact time it
- /// takes before a scheduled orchestration starts running depends on several factors, including the configuration
- /// and health of the backend task hub, and whether a start time was provided via .
- ///
- /// The task associated with this method completes after the orchestration instance was successfully scheduled. You
- /// can use the to query the status of the
- /// scheduled instance, the method to wait
- /// for the instance to transition out of the status, or the
- /// method to wait for the instance to
- /// reach a terminal state (Completed, Terminated, Failed, etc.).
- ///
- ///
- /// The name of the orchestrator to schedule.
- ///
- /// The optional input to pass to the scheduled orchestration instance. This must be a serializable value.
- ///
- /// The options to start the new orchestration with.
- ///
- /// The cancellation token. This only cancels enqueueing the new orchestration to the backend. Does not cancel the
- /// orchestration once enqueued.
- ///
- ///
- /// A task that completes when the orchestration instance is successfully scheduled. The value of this task is
- /// the instance ID of the scheduled orchestration instance. If a non-null instance ID was provided via
- /// , the same value will be returned by the completed task.
- ///
- /// Thrown if is empty.
- public abstract Task ScheduleNewOrchestrationInstanceAsync(
- TaskName orchestratorName,
- object? input = null,
- StartOrchestrationOptions? options = null,
- CancellationToken cancellation = default);
-
- ///
- public virtual Task RaiseEventAsync(
- string instanceId, string eventName, CancellationToken cancellation)
- => this.RaiseEventAsync(instanceId, eventName, null, cancellation);
-
- ///
- /// Sends an event notification message to a waiting orchestration instance.
- ///
- ///
- ///
- /// In order to handle the event, the target orchestration instance must be waiting for an
- /// event named using the
- /// API.
- /// If the target orchestration instance is not yet waiting for an event named ,
- /// then the event will be saved in the orchestration instance state and dispatched immediately when the
- /// orchestrator calls .
- /// This event saving occurs even if the orchestrator has canceled its wait operation before the event was received.
- ///
- /// Orchestrators can wait for the same event name multiple times, so sending multiple events with the same name is
- /// allowed. Each external event received by an orchestrator will complete just one task returned by the
- /// method.
- ///
- /// Raised events for a completed or non-existent orchestration instance will be silently discarded.
- ///
- ///
- /// The ID of the orchestration instance that will handle the event.
- /// The name of the event. Event names are case-insensitive.
- /// The serializable data payload to include with the event.
- ///
- /// The cancellation token. This only cancels enqueueing the event to the backend. Does not abort sending the event
- /// once enqueued.
- ///
- /// A task that completes when the event notification message has been enqueued.
- ///
- /// Thrown if or is null or empty.
- ///
- public abstract Task RaiseEventAsync(
- string instanceId, string eventName, object? eventPayload = null, CancellationToken cancellation = default);
-
- ///
- public virtual Task WaitForInstanceStartAsync(
- string instanceId, CancellationToken cancellation)
- => this.WaitForInstanceStartAsync(instanceId, false, cancellation);
-
- ///
- /// Waits for an orchestration to start running and returns a
- /// object that contains metadata about the started instance.
- ///
- ///
- ///
- /// A "started" orchestration instance is any instance not in the
- /// state.
- ///
- /// If an orchestration instance is already running when this method is called, the method will return immediately.
- ///
- ///
- /// The unique ID of the orchestration instance to wait for.
- ///
- /// Specify true to fetch the orchestration instance's inputs, outputs, and custom status, or false to
- /// omit them. The default value is false to minimize the network bandwidth, serialization, and memory costs
- /// associated with fetching the instance metadata.
- ///
- /// A that can be used to cancel the wait operation.
- ///
- /// Returns a record that describes the orchestration instance and its execution
- /// status or null if no instance with ID is found.
- ///
- public abstract Task WaitForInstanceStartAsync(
- string instanceId, bool getInputsAndOutputs = false, CancellationToken cancellation = default);
-
- ///
- public virtual Task WaitForInstanceCompletionAsync(
- string instanceId, CancellationToken cancellation)
- => this.WaitForInstanceCompletionAsync(instanceId, false, cancellation);
-
- ///
- /// Waits for an orchestration to complete and returns a
- /// object that contains metadata about the started instance.
- ///
- ///
- ///
- /// A "completed" orchestration instance is any instance in one of the terminal states. For example, the
- /// , , or
- /// states.
- ///
- /// Orchestrations are long-running and could take hours, days, or months before completing.
- /// Orchestrations can also be eternal, in which case they'll never complete unless terminated.
- /// In such cases, this call may block indefinitely, so care must be taken to ensure appropriate timeouts are
- /// enforced using the parameter.
- ///
- /// If an orchestration instance is already complete when this method is called, the method will return immediately.
- ///
- ///
- ///
- public abstract Task WaitForInstanceCompletionAsync(
- string instanceId, bool getInputsAndOutputs = false, CancellationToken cancellation = default);
-
- ///
- public virtual Task TerminateInstanceAsync(string instanceId, CancellationToken cancellation)
- => this.TerminateInstanceAsync(instanceId, null, cancellation);
-
- ///
- public virtual Task TerminateInstanceAsync(string instanceId, object? output, CancellationToken cancellation = default)
- {
- TerminateInstanceOptions? options = output is null ? null : new() { Output = output };
- return this.TerminateInstanceAsync(instanceId, options, cancellation);
- }
-
- ///
- /// Terminates an orchestration instance and updates its runtime status to
- /// .
- ///
- ///
- ///
- /// This method internally enqueues a "terminate" message in the task hub. When the task hub worker processes
- /// this message, it will update the runtime status of the target instance to
- /// . You can use the
- /// to wait for the instance to reach
- /// the terminated state.
- ///
- ///
- /// Terminating an orchestration by default will not terminate any of the child sub-orchestrations that were started by
- /// the orchetration instance. If you want to terminate sub-orchestration instances as well, you can set
- /// flag to true which will enable termination of child sub-orchestration instances. It is set to false by default.
- /// Terminating an orchestration instance has no effect on any in-flight activity function executions
- /// that were started by the terminated instance. Those actions will continue to run
- /// without interruption. However, their results will be discarded.
- ///
- /// At the time of writing, there is no way to terminate an in-flight activity execution.
- ///
- /// Attempting to terminate a completed or non-existent orchestration instance will fail silently.
- ///
- ///
- /// The ID of the orchestration instance to terminate.
- /// The optional options for terminating the orchestration.
- ///
- /// The cancellation token. This only cancels enqueueing the termination request to the backend. Does not abort
- /// termination of the orchestration once enqueued.
- ///
- /// A task that completes when the terminate message is enqueued.
- public virtual Task TerminateInstanceAsync(string instanceId, TerminateInstanceOptions? options = null, CancellationToken cancellation = default)
- => throw new NotSupportedException($"{this.GetType()} does not support orchestration termination.");
-
- ///
- public virtual Task SuspendInstanceAsync(string instanceId, CancellationToken cancellation)
- => this.SuspendInstanceAsync(instanceId, null, cancellation);
-
- ///
- /// Suspends an orchestration instance, halting processing of it until
- /// is used to resume the orchestration.
- ///
- /// The instance ID of the orchestration to suspend.
- /// The optional suspension reason.
- ///
- /// A that can be used to cancel the suspend operation. Note, cancelling this token
- /// does not resume the orchestration if suspend was successful.
- ///
- /// A task that completes when the suspend has been committed to the backend.
- public abstract Task SuspendInstanceAsync(
- string instanceId, string? reason = null, CancellationToken cancellation = default);
-
- ///
- public virtual Task ResumeInstanceAsync(string instanceId, CancellationToken cancellation)
- => this.ResumeInstanceAsync(instanceId, null, cancellation);
-
- ///
- /// Resumes an orchestration instance that was suspended via .
- ///
- /// The instance ID of the orchestration to resume.
- /// The optional resume reason.
- ///
- /// A that can be used to cancel the resume operation. Note, cancelling this token
- /// does not re-suspend the orchestration if resume was successful.
- ///
- /// A task that completes when the resume has been committed to the backend.
- public abstract Task ResumeInstanceAsync(
- string instanceId, string? reason = null, CancellationToken cancellation = default);
-
- ///
- public virtual Task GetInstanceAsync(
- string instanceId, CancellationToken cancellation)
- => this.GetInstanceAsync(instanceId, false, cancellation);
-
- ///
- /// Fetches orchestration instance metadata from the configured durable store.
- ///
- ///
- /// You can use the parameter to determine whether to fetch input and
- /// output data for the target orchestration instance. If your code doesn't require access to this data, it's
- /// recommended that you set this parameter to false to minimize the network bandwidth, serialization, and
- /// memory costs associated with fetching the instance metadata.
- ///
- ///
- public virtual Task GetInstanceAsync(
- string instanceId, bool getInputsAndOutputs = false, CancellationToken cancellation = default)
- => this.GetInstancesAsync(instanceId, getInputsAndOutputs, cancellation);
-
- ///
- [EditorBrowsable(EditorBrowsableState.Never)] // use GetInstanceAsync
- public virtual Task GetInstancesAsync(
- string instanceId, CancellationToken cancellation)
- => this.GetInstancesAsync(instanceId, false, cancellation);
-
- ///
- /// Fetches orchestration instance metadata from the configured durable store.
- ///
- ///
- /// You can use the parameter to determine whether to fetch input and
- /// output data for the target orchestration instance. If your code doesn't require access to this data, it's
- /// recommended that you set this parameter to false to minimize the network bandwidth, serialization, and
- /// memory costs associated with fetching the instance metadata.
- ///
- ///
- [EditorBrowsable(EditorBrowsableState.Never)] // use GetInstanceAsync
- public abstract Task GetInstancesAsync(
- string instanceId, bool getInputsAndOutputs = false, CancellationToken cancellation = default);
-
- ///
- /// Queries orchestration instances.
- ///
- /// Filters down the instances included in the query.
- /// An async pageable of the query results.
- public abstract AsyncPageable GetAllInstancesAsync(OrchestrationQuery? filter = null);
-
- ///
- public virtual Task PurgeInstanceAsync(string instanceId, CancellationToken cancellation)
- => this.PurgeInstanceAsync(instanceId, null, cancellation);
-
- ///
- /// Purges orchestration instance metadata from the durable store.
- ///
- ///
- ///
- /// This method can be used to permanently delete orchestration metadata from the underlying storage provider,
- /// including any stored inputs, outputs, and orchestration history records. This is often useful for implementing
- /// data retention policies and for keeping storage costs minimal. Only orchestration instances in the
- /// , , or
- /// state can be purged.
- ///
- /// Purging an orchestration will by default not purge any of the child sub-orchestrations that were started by the
- /// orchetration instance. Currently, purging of sub-orchestrations is not supported.
- /// If is not found in the data store, or if the instance is found but not in a
- /// terminal state, then the returned object will have a
- /// value of 0. Otherwise, the existing data will be purged and
- /// will be the count of purged instances.
- ///
- ///
- /// The unique ID of the orchestration instance to purge.
- /// The optional options for purging the orchestration.
- ///
- /// A that can be used to cancel the purge operation.
- ///
- ///
- /// This method returns a object after the operation has completed with a
- /// indicating the number of orchestration instances that were purged,
- /// including the count of sub-orchestrations purged if any.
- ///
- public virtual Task PurgeInstanceAsync(
- string instanceId, PurgeInstanceOptions? options = null, CancellationToken cancellation = default)
- {
- throw new NotSupportedException($"{this.GetType()} does not support purging of orchestration instances.");
- }
-
- ///
- public virtual Task PurgeAllInstancesAsync(PurgeInstancesFilter filter, CancellationToken cancellation)
- => this.PurgeAllInstancesAsync(filter, null, cancellation);
-
- ///
- /// Purges orchestration instances metadata from the durable store.
- ///
- /// The filter for which orchestrations to purge.
- /// The optional options for purging the orchestration.
- ///
- /// A that can be used to cancel the purge operation.
- ///
- ///
- /// This method returns a object after the operation has completed with a
- /// indicating the number of orchestration instances that were purged.
- ///
- public virtual Task PurgeAllInstancesAsync(
- PurgeInstancesFilter filter, PurgeInstanceOptions? options = null, CancellationToken cancellation = default)
- {
- throw new NotSupportedException($"{this.GetType()} does not support purging of orchestration instances.");
- }
-
- ///
- /// Restarts an orchestration instance with the same or a new instance ID.
- ///
- ///
- ///
- /// This method restarts an existing orchestration instance. If is true,
- /// a new instance ID will be generated for the restarted orchestration. If false, the original instance ID will be reused.
- ///
- /// The restarted orchestration will use the same input data as the original instance. If the original orchestration
- /// instance is not found, an will be thrown.
- ///
- /// Note that this operation is backend-specific and may not be supported by all durable task backends.
- /// If the backend does not support restart operations, a will be thrown.
- ///
- ///
- /// The ID of the orchestration instance to restart.
- ///
- /// If true, a new instance ID will be generated for the restarted orchestration.
- /// If false, the original instance ID will be reused.
- ///
- ///
- /// The cancellation token. This only cancels enqueueing the restart request to the backend.
- /// Does not abort restarting the orchestration once enqueued.
- ///
- ///
- /// A task that completes when the orchestration instance is successfully restarted.
- /// The value of this task is the instance ID of the restarted orchestration instance.
- ///
- ///
- /// Thrown if an orchestration with the specified was not found.
- ///
- /// Thrown when attempting to restart an instance using the same instance Id
- /// while the instance has not yet reached a completed or terminal state.
- ///
- /// Thrown if the backend does not support restart operations.
- public virtual Task RestartAsync(
- string instanceId,
- bool restartWithNewInstanceId = false,
- CancellationToken cancellation = default)
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.ComponentModel;
+using Microsoft.DurableTask.Client.Entities;
+using Microsoft.DurableTask.Internal;
+using DurableTask.Core.History;
+
+namespace Microsoft.DurableTask.Client;
+
+///
+/// Base class that defines client operations for managing durable task instances.
+///
+///
+///
+/// Instances of can be used to start, query, raise events to, and terminate
+/// orchestration instances. In most cases, methods on this class accept an instance ID as a parameter, which identifies
+/// the orchestration instance.
+///
+/// At the time of writing, the most common implementation of this class is the gRPC client, which works by making gRPC
+/// calls to a remote service (e.g. a sidecar) that implements the operation behavior. To ensure any owned network
+/// resources are properly released, instances of should be disposed when they are no
+/// longer needed.
+///
+/// Instances of this class are expected to be safe for multithreaded apps. You can therefore safely cache instances
+/// of this class and reuse them across multiple contexts. Caching these objects is useful to improve overall
+/// performance.
+///
+///
+public abstract class DurableTaskClient : IOrchestrationSubmitter, IAsyncDisposable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the client.
+ protected DurableTaskClient(string name)
+ {
+ this.Name = name;
+ }
+
+ ///
+ /// Gets the name of the client.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the for interacting with durable entities.
+ ///
+ ///
+ /// Not all clients support durable entities. Refer to a specific client implementation for verifying support.
+ ///
+ public virtual DurableEntityClient Entities =>
+ throw new NotSupportedException($"{this.GetType()} does not support durable entities.");
+
+ ///
+ public virtual Task ScheduleNewOrchestrationInstanceAsync(
+ TaskName orchestratorName, CancellationToken cancellation)
+ => this.ScheduleNewOrchestrationInstanceAsync(orchestratorName, null, null, cancellation);
+
+ ///
+ public virtual Task ScheduleNewOrchestrationInstanceAsync(
+ TaskName orchestratorName, object? input, CancellationToken cancellation)
+ => this.ScheduleNewOrchestrationInstanceAsync(orchestratorName, input, null, cancellation);
+
+ ///
+ public virtual Task ScheduleNewOrchestrationInstanceAsync(
+ TaskName orchestratorName, StartOrchestrationOptions options, CancellationToken cancellation = default)
+ => this.ScheduleNewOrchestrationInstanceAsync(orchestratorName, null, options, cancellation);
+
+ ///
+ /// Schedules a new orchestration instance for execution.
+ ///
+ ///
+ /// All orchestrations must have a unique instance ID. You can provide an instance ID using the
+ /// parameter or you can omit this and a random instance ID will be
+ /// generated for you automatically. If an orchestration with the specified instance ID already exists and is in a
+ /// non-terminal state (Pending, Running, etc.), then this operation may fail silently. However, if an orchestration
+ /// instance with this ID already exists in a terminal state (Completed, Terminated, Failed, etc.) then the instance
+ /// may be recreated automatically, depending on the configuration of the backend instance store.
+ ///
+ /// Orchestration instances started with this method will be created in the
+ /// state and will transition to the
+ /// after successfully awaiting its first task. The exact time it
+ /// takes before a scheduled orchestration starts running depends on several factors, including the configuration
+ /// and health of the backend task hub, and whether a start time was provided via .
+ ///
+ /// The task associated with this method completes after the orchestration instance was successfully scheduled. You
+ /// can use the to query the status of the
+ /// scheduled instance, the method to wait
+ /// for the instance to transition out of the status, or the
+ /// method to wait for the instance to
+ /// reach a terminal state (Completed, Terminated, Failed, etc.).
+ ///
+ ///
+ /// The name of the orchestrator to schedule.
+ ///
+ /// The optional input to pass to the scheduled orchestration instance. This must be a serializable value.
+ ///
+ /// The options to start the new orchestration with.
+ ///
+ /// The cancellation token. This only cancels enqueueing the new orchestration to the backend. Does not cancel the
+ /// orchestration once enqueued.
+ ///
+ ///
+ /// A task that completes when the orchestration instance is successfully scheduled. The value of this task is
+ /// the instance ID of the scheduled orchestration instance. If a non-null instance ID was provided via
+ /// , the same value will be returned by the completed task.
+ ///
+ /// Thrown if is empty.
+ public abstract Task ScheduleNewOrchestrationInstanceAsync(
+ TaskName orchestratorName,
+ object? input = null,
+ StartOrchestrationOptions? options = null,
+ CancellationToken cancellation = default);
+
+ ///
+ public virtual Task RaiseEventAsync(
+ string instanceId, string eventName, CancellationToken cancellation)
+ => this.RaiseEventAsync(instanceId, eventName, null, cancellation);
+
+ ///
+ /// Sends an event notification message to a waiting orchestration instance.
+ ///
+ ///
+ ///
+ /// In order to handle the event, the target orchestration instance must be waiting for an
+ /// event named using the
+ /// API.
+ /// If the target orchestration instance is not yet waiting for an event named ,
+ /// then the event will be saved in the orchestration instance state and dispatched immediately when the
+ /// orchestrator calls .
+ /// This event saving occurs even if the orchestrator has canceled its wait operation before the event was received.
+ ///
+ /// Orchestrators can wait for the same event name multiple times, so sending multiple events with the same name is
+ /// allowed. Each external event received by an orchestrator will complete just one task returned by the
+ /// method.
+ ///
+ /// Raised events for a completed or non-existent orchestration instance will be silently discarded.
+ ///
+ ///
+ /// The ID of the orchestration instance that will handle the event.
+ /// The name of the event. Event names are case-insensitive.
+ /// The serializable data payload to include with the event.
+ ///
+ /// The cancellation token. This only cancels enqueueing the event to the backend. Does not abort sending the event
+ /// once enqueued.
+ ///
+ /// A task that completes when the event notification message has been enqueued.
+ ///
+ /// Thrown if or is null or empty.
+ ///
+ public abstract Task RaiseEventAsync(
+ string instanceId, string eventName, object? eventPayload = null, CancellationToken cancellation = default);
+
+ ///
+ public virtual Task WaitForInstanceStartAsync(
+ string instanceId, CancellationToken cancellation)
+ => this.WaitForInstanceStartAsync(instanceId, false, cancellation);
+
+ ///
+ /// Waits for an orchestration to start running and returns a
+ /// object that contains metadata about the started instance.
+ ///
+ ///
+ ///
+ /// A "started" orchestration instance is any instance not in the
+ /// state.
+ ///
+ /// If an orchestration instance is already running when this method is called, the method will return immediately.
+ ///
+ ///
+ /// The unique ID of the orchestration instance to wait for.
+ ///
+ /// Specify true to fetch the orchestration instance's inputs, outputs, and custom status, or false to
+ /// omit them. The default value is false to minimize the network bandwidth, serialization, and memory costs
+ /// associated with fetching the instance metadata.
+ ///
+ /// A that can be used to cancel the wait operation.
+ ///
+ /// Returns a record that describes the orchestration instance and its execution
+ /// status or null if no instance with ID is found.
+ ///
+ public abstract Task WaitForInstanceStartAsync(
+ string instanceId, bool getInputsAndOutputs = false, CancellationToken cancellation = default);
+
+ ///
+ public virtual Task WaitForInstanceCompletionAsync(
+ string instanceId, CancellationToken cancellation)
+ => this.WaitForInstanceCompletionAsync(instanceId, false, cancellation);
+
+ ///
+ /// Waits for an orchestration to complete and returns a
+ /// object that contains metadata about the started instance.
+ ///
+ ///
+ ///
+ /// A "completed" orchestration instance is any instance in one of the terminal states. For example, the
+ /// , , or
+ /// states.
+ ///
+ /// Orchestrations are long-running and could take hours, days, or months before completing.
+ /// Orchestrations can also be eternal, in which case they'll never complete unless terminated.
+ /// In such cases, this call may block indefinitely, so care must be taken to ensure appropriate timeouts are
+ /// enforced using the parameter.
+ ///
+ /// If an orchestration instance is already complete when this method is called, the method will return immediately.
+ ///
+ ///
+ ///
+ public abstract Task WaitForInstanceCompletionAsync(
+ string instanceId, bool getInputsAndOutputs = false, CancellationToken cancellation = default);
+
+ ///
+ public virtual Task TerminateInstanceAsync(string instanceId, CancellationToken cancellation)
+ => this.TerminateInstanceAsync(instanceId, null, cancellation);
+
+ ///
+ public virtual Task TerminateInstanceAsync(string instanceId, object? output, CancellationToken cancellation = default)
+ {
+ TerminateInstanceOptions? options = output is null ? null : new() { Output = output };
+ return this.TerminateInstanceAsync(instanceId, options, cancellation);
+ }
+
+ ///
+ /// Terminates an orchestration instance and updates its runtime status to
+ /// .
+ ///
+ ///
+ ///
+ /// This method internally enqueues a "terminate" message in the task hub. When the task hub worker processes
+ /// this message, it will update the runtime status of the target instance to
+ /// . You can use the
+ /// to wait for the instance to reach
+ /// the terminated state.
+ ///
+ ///
+ /// Terminating an orchestration by default will not terminate any of the child sub-orchestrations that were started by
+ /// the orchetration instance. If you want to terminate sub-orchestration instances as well, you can set
+ /// flag to true which will enable termination of child sub-orchestration instances. It is set to false by default.
+ /// Terminating an orchestration instance has no effect on any in-flight activity function executions
+ /// that were started by the terminated instance. Those actions will continue to run
+ /// without interruption. However, their results will be discarded.
+ ///
+ /// At the time of writing, there is no way to terminate an in-flight activity execution.
+ ///
+ /// Attempting to terminate a completed or non-existent orchestration instance will fail silently.
+ ///
+ ///
+ /// The ID of the orchestration instance to terminate.
+ /// The optional options for terminating the orchestration.
+ ///
+ /// The cancellation token. This only cancels enqueueing the termination request to the backend. Does not abort
+ /// termination of the orchestration once enqueued.
+ ///
+ /// A task that completes when the terminate message is enqueued.
+ public virtual Task TerminateInstanceAsync(string instanceId, TerminateInstanceOptions? options = null, CancellationToken cancellation = default)
+ => throw new NotSupportedException($"{this.GetType()} does not support orchestration termination.");
+
+ ///
+ public virtual Task SuspendInstanceAsync(string instanceId, CancellationToken cancellation)
+ => this.SuspendInstanceAsync(instanceId, null, cancellation);
+
+ ///
+ /// Suspends an orchestration instance, halting processing of it until
+ /// is used to resume the orchestration.
+ ///
+ /// The instance ID of the orchestration to suspend.
+ /// The optional suspension reason.
+ ///
+ /// A that can be used to cancel the suspend operation. Note, cancelling this token
+ /// does not resume the orchestration if suspend was successful.
+ ///
+ /// A task that completes when the suspend has been committed to the backend.
+ public abstract Task SuspendInstanceAsync(
+ string instanceId, string? reason = null, CancellationToken cancellation = default);
+
+ ///
+ public virtual Task ResumeInstanceAsync(string instanceId, CancellationToken cancellation)
+ => this.ResumeInstanceAsync(instanceId, null, cancellation);
+
+ ///
+ /// Resumes an orchestration instance that was suspended via .
+ ///
+ /// The instance ID of the orchestration to resume.
+ /// The optional resume reason.
+ ///
+ /// A that can be used to cancel the resume operation. Note, cancelling this token
+ /// does not re-suspend the orchestration if resume was successful.
+ ///
+ /// A task that completes when the resume has been committed to the backend.
+ public abstract Task ResumeInstanceAsync(
+ string instanceId, string? reason = null, CancellationToken cancellation = default);
+
+ ///
+ public virtual Task GetInstanceAsync(
+ string instanceId, CancellationToken cancellation)
+ => this.GetInstanceAsync(instanceId, false, cancellation);
+
+ ///
+ /// Fetches orchestration instance metadata from the configured durable store.
+ ///
+ ///
+ /// You can use the parameter to determine whether to fetch input and
+ /// output data for the target orchestration instance. If your code doesn't require access to this data, it's
+ /// recommended that you set this parameter to false to minimize the network bandwidth, serialization, and
+ /// memory costs associated with fetching the instance metadata.
+ ///
+ ///
+ public virtual Task GetInstanceAsync(
+ string instanceId, bool getInputsAndOutputs = false, CancellationToken cancellation = default)
+ => this.GetInstancesAsync(instanceId, getInputsAndOutputs, cancellation);
+
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)] // use GetInstanceAsync
+ public virtual Task GetInstancesAsync(
+ string instanceId, CancellationToken cancellation)
+ => this.GetInstancesAsync(instanceId, false, cancellation);
+
+ ///
+ /// Fetches orchestration instance metadata from the configured durable store.
+ ///
+ ///
+ /// You can use the parameter to determine whether to fetch input and
+ /// output data for the target orchestration instance. If your code doesn't require access to this data, it's
+ /// recommended that you set this parameter to false to minimize the network bandwidth, serialization, and
+ /// memory costs associated with fetching the instance metadata.
+ ///
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)] // use GetInstanceAsync
+ public abstract Task GetInstancesAsync(
+ string instanceId, bool getInputsAndOutputs = false, CancellationToken cancellation = default);
+
+ ///
+ /// Queries orchestration instances.
+ ///
+ /// Filters down the instances included in the query.
+ /// An async pageable of the query results.
+ public abstract AsyncPageable GetAllInstancesAsync(OrchestrationQuery? filter = null);
+
+ ///
+ public virtual Task PurgeInstanceAsync(string instanceId, CancellationToken cancellation)
+ => this.PurgeInstanceAsync(instanceId, null, cancellation);
+
+ ///
+ /// Purges orchestration instance metadata from the durable store.
+ ///
+ ///
+ ///
+ /// This method can be used to permanently delete orchestration metadata from the underlying storage provider,
+ /// including any stored inputs, outputs, and orchestration history records. This is often useful for implementing
+ /// data retention policies and for keeping storage costs minimal. Only orchestration instances in the
+ /// , , or
+ /// state can be purged.
+ ///
+ /// Purging an orchestration will by default not purge any of the child sub-orchestrations that were started by the
+ /// orchetration instance. Currently, purging of sub-orchestrations is not supported.
+ /// If is not found in the data store, or if the instance is found but not in a
+ /// terminal state, then the returned object will have a
+ /// value of 0. Otherwise, the existing data will be purged and
+ /// will be the count of purged instances.
+ ///
+ ///
+ /// The unique ID of the orchestration instance to purge.
+ /// The optional options for purging the orchestration.
+ ///
+ /// A that can be used to cancel the purge operation.
+ ///
+ ///
+ /// This method returns a object after the operation has completed with a
+ /// indicating the number of orchestration instances that were purged,
+ /// including the count of sub-orchestrations purged if any.
+ ///
+ public virtual Task PurgeInstanceAsync(
+ string instanceId, PurgeInstanceOptions? options = null, CancellationToken cancellation = default)
+ {
+ throw new NotSupportedException($"{this.GetType()} does not support purging of orchestration instances.");
+ }
+
+ ///
+ public virtual Task PurgeAllInstancesAsync(PurgeInstancesFilter filter, CancellationToken cancellation)
+ => this.PurgeAllInstancesAsync(filter, null, cancellation);
+
+ ///
+ /// Purges orchestration instances metadata from the durable store.
+ ///
+ /// The filter for which orchestrations to purge.
+ /// The optional options for purging the orchestration.
+ ///
+ /// A that can be used to cancel the purge operation.
+ ///
+ ///
+ /// This method returns a object after the operation has completed with a
+ /// indicating the number of orchestration instances that were purged.
+ ///
+ public virtual Task PurgeAllInstancesAsync(
+ PurgeInstancesFilter filter, PurgeInstanceOptions? options = null, CancellationToken cancellation = default)
+ {
+ throw new NotSupportedException($"{this.GetType()} does not support purging of orchestration instances.");
+ }
+
+ ///
+ /// Restarts an orchestration instance with the same or a new instance ID.
+ ///
+ ///
+ ///
+ /// This method restarts an existing orchestration instance. If is true,
+ /// a new instance ID will be generated for the restarted orchestration. If false, the original instance ID will be reused.
+ ///
+ /// The restarted orchestration will use the same input data as the original instance. If the original orchestration
+ /// instance is not found, an will be thrown.
+ ///
+ /// Note that this operation is backend-specific and may not be supported by all durable task backends.
+ /// If the backend does not support restart operations, a will be thrown.
+ ///
+ ///
+ /// The ID of the orchestration instance to restart.
+ ///
+ /// If true, a new instance ID will be generated for the restarted orchestration.
+ /// If false, the original instance ID will be reused.
+ ///
+ ///
+ /// The cancellation token. This only cancels enqueueing the restart request to the backend.
+ /// Does not abort restarting the orchestration once enqueued.
+ ///
+ ///
+ /// A task that completes when the orchestration instance is successfully restarted.
+ /// The value of this task is the instance ID of the restarted orchestration instance.
+ ///
+ ///
+ /// Thrown if an orchestration with the specified was not found.
+ ///
+ /// Thrown when attempting to restart an instance using the same instance Id
+ /// while the instance has not yet reached a completed or terminal state.
+ ///
+ /// Thrown if the backend does not support restart operations.
+ public virtual Task RestartAsync(
+ string instanceId,
+ bool restartWithNewInstanceId = false,
+ CancellationToken cancellation = default)
=> throw new NotSupportedException($"{this.GetType()} does not support orchestration restart.");
///
@@ -450,7 +451,7 @@ public virtual Task RestartAsync(
///
/// The instance ID of the orchestration to rewind.
/// The reason for the rewind.
- /// The cancellation token. This only cancels enqueueing the rewind request to the backend.
+ /// The cancellation token. This only cancels enqueueing the rewind request to the backend.
/// It does not abort rewinding the orchestration once the request has been enqueued.
/// A task that represents the enqueueing of the rewind operation.
/// Thrown if this implementation of does not
@@ -464,15 +465,61 @@ public virtual Task RewindInstanceAsync(
string instanceId,
string reason,
CancellationToken cancellation = default)
- => throw new NotSupportedException($"{this.GetType()} does not support orchestration rewind.");
-
- // TODO: Create task hub
-
- // TODO: Delete task hub
-
- ///
- /// Disposes any unmanaged resources associated with this .
- ///
- /// A that completes when the disposal completes.
- public abstract ValueTask DisposeAsync();
-}
+ => throw new NotSupportedException($"{this.GetType()} does not support orchestration rewind.");
+
+ ///
+ /// Lists orchestration instance IDs filtered by completed time.
+ ///
+ /// The runtime statuses to filter by.
+ /// The start time for completed time filter (inclusive).
+ /// The end time for completed time filter (inclusive).
+ /// The page size for pagination.
+ /// The last fetched instance key.
+ /// The cancellation token.
+ /// A page of instance IDs with continuation token.
+ public virtual Task> ListInstanceIdsAsync(
+ IEnumerable? runtimeStatus = null,
+ DateTimeOffset? completedTimeFrom = null,
+ DateTimeOffset? completedTimeTo = null,
+ int pageSize = OrchestrationQuery.DefaultPageSize,
+ string? lastInstanceKey = null,
+ CancellationToken cancellation = default)
+ {
+ throw new NotSupportedException($"{this.GetType()} does not support listing orchestration instance IDs by completed time.");
+ }
+
+ ///
+ /// Streams the execution history events for an orchestration instance.
+ ///
+ ///
+ /// This method returns an async enumerable that yields history events as they are retrieved from the backend.
+ /// The history events are returned in chronological order and include all events that occurred during the
+ /// orchestration instance's execution.
+ ///
+ /// The instance ID of the orchestration to stream history for.
+ /// The optional execution ID. If null, the latest execution will be used.
+ ///
+ /// A that can be used to cancel the streaming operation.
+ ///
+ /// An async enumerable of objects.
+ /// Thrown if an orchestration with the specified does not exist.
+ /// Thrown if the operation is canceled via the token.
+ public virtual IAsyncEnumerable StreamInstanceHistoryAsync(
+ string instanceId,
+ string? executionId = null,
+ CancellationToken cancellation = default)
+
+ {
+ throw new NotSupportedException($"{this.GetType()} does not support streaming instance history.");
+ }
+
+ // TODO: Create task hub
+
+ // TODO: Delete task hub
+
+ ///
+ /// Disposes any unmanaged resources associated with this .
+ ///
+ /// A that completes when the disposal completes.
+ public abstract ValueTask DisposeAsync();
+}
\ No newline at end of file
diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs
index efc6d765c..b55fe2378 100644
--- a/src/Client/Grpc/GrpcDurableTaskClient.cs
+++ b/src/Client/Grpc/GrpcDurableTaskClient.cs
@@ -3,7 +3,9 @@
using System.Diagnostics;
using System.Text;
+using DurableTask.Core.History;
using Google.Protobuf.WellKnownTypes;
+using Grpc.Core;
using Microsoft.DurableTask.Client.Entities;
using Microsoft.DurableTask.Tracing;
using Microsoft.Extensions.DependencyInjection;
@@ -323,6 +325,53 @@ public override async Task WaitForInstanceStartAsync(
}
}
+ ///
+ public override async Task> ListInstanceIdsAsync(
+ IEnumerable? runtimeStatus = null,
+ DateTimeOffset? completedTimeFrom = null,
+ DateTimeOffset? completedTimeTo = null,
+ int pageSize = OrchestrationQuery.DefaultPageSize,
+ string? lastInstanceKey = null,
+ CancellationToken cancellation = default)
+ {
+ Check.NotEntity(this.options.EnableEntitySupport, null);
+
+ P.ListInstanceIdsRequest request = new()
+ {
+ PageSize = pageSize,
+ LastInstanceKey = lastInstanceKey ?? string.Empty,
+ };
+
+ if (runtimeStatus != null)
+ {
+ request.RuntimeStatus.AddRange(runtimeStatus.Select(x => x.ToGrpcStatus()));
+ }
+
+ if (completedTimeFrom.HasValue)
+ {
+ request.CompletedTimeFrom = completedTimeFrom.Value.ToTimestamp();
+ }
+
+ if (completedTimeTo.HasValue)
+ {
+ request.CompletedTimeTo = completedTimeTo.Value.ToTimestamp();
+ }
+
+ try
+ {
+ P.ListInstanceIdsResponse response = await this.sidecarClient.ListInstanceIdsAsync(
+ request,
+ cancellationToken: cancellation);
+
+ return new Page(response.InstanceIds.ToList(), response.LastInstanceKey);
+ }
+ catch (RpcException e) when (e.StatusCode == StatusCode.Cancelled)
+ {
+ throw new OperationCanceledException(
+ $"The {nameof(this.ListInstanceIdsAsync)} operation was canceled.", e, cancellation);
+ }
+ }
+
///
public override async Task WaitForInstanceCompletionAsync(
string instanceId, bool getInputsAndOutputs = false, CancellationToken cancellation = default)
@@ -468,6 +517,64 @@ public override async Task RewindInstanceAsync(
}
}
+ ///
+ public override async IAsyncEnumerable StreamInstanceHistoryAsync(
+ string instanceId,
+ string? executionId = null,
+ CancellationToken cancellation = default)
+ {
+ Check.NotNullOrEmpty(instanceId);
+ Check.NotEntity(this.options.EnableEntitySupport, instanceId);
+
+ P.StreamInstanceHistoryRequest request = new()
+ {
+ InstanceId = instanceId,
+ ExecutionId = executionId,
+ ForWorkItemProcessing = false,
+ };
+
+ using AsyncServerStreamingCall streamResponse =
+ this.sidecarClient.StreamInstanceHistory(request, cancellationToken: cancellation);
+
+ IAsyncStreamReader responseStream = streamResponse.ResponseStream;
+
+ // Create conversion state to maintain orchestration instance across events
+ // This is required for entity-related events (EntityOperationCalled, EntityLockRequested, etc.)
+ // which need the parent orchestration instance information from ExecutionStartedEvent
+ Microsoft.DurableTask.ProtoUtils.EntityConversionState conversionState = new(insertMissingEntityUnlocks: false);
+
+ bool hasMore = true;
+ while (hasMore)
+ {
+ bool moveNextResult;
+ try
+ {
+ moveNextResult = await responseStream.MoveNext(cancellation).ConfigureAwait(false);
+ }
+ catch (RpcException e) when (e.StatusCode == StatusCode.Cancelled)
+ {
+ throw new OperationCanceledException(
+ $"The {nameof(this.StreamInstanceHistoryAsync)} operation was canceled.", e, cancellation);
+ }
+ catch (RpcException e) when (e.StatusCode == StatusCode.NotFound)
+ {
+ throw new ArgumentException($"An orchestration with the instanceId {instanceId} was not found.", e);
+ }
+
+ hasMore = moveNextResult;
+ if (hasMore)
+ {
+ P.HistoryChunk chunk = responseStream.Current;
+ foreach (P.HistoryEvent protoEvent in chunk.Events)
+ {
+ // Use the conversion state's converter to maintain state across events
+ // This ensures entity events can access the orchestration instance from ExecutionStartedEvent
+ yield return conversionState.ConvertFromProto(protoEvent);
+ }
+ }
+ }
+ }
+
static AsyncDisposable GetCallInvoker(GrpcDurableTaskClientOptions options, out CallInvoker callInvoker)
{
if (options.Channel is GrpcChannel c)
diff --git a/src/ExportHistory/Activities/ExportInstanceHistoryActivity.cs b/src/ExportHistory/Activities/ExportInstanceHistoryActivity.cs
new file mode 100644
index 000000000..bc32dd40f
--- /dev/null
+++ b/src/ExportHistory/Activities/ExportInstanceHistoryActivity.cs
@@ -0,0 +1,321 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.IO.Compression;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using DurableTask.Core.History;
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.DurableTask.ExportHistory;
+
+///
+/// Activity that exports one orchestration instance history to the configured blob destination.
+///
+///
+/// Initializes a new instance of the class.
+///
+[DurableTask]
+public class ExportInstanceHistoryActivity(
+ DurableTaskClient client,
+ ILogger logger,
+ IOptions storageOptions) : TaskActivity
+{
+ readonly DurableTaskClient client = Check.NotNull(client, nameof(client));
+ readonly ILogger logger = Check.NotNull(logger, nameof(logger));
+ readonly ExportHistoryStorageOptions storageOptions = Check.NotNull(storageOptions?.Value, nameof(storageOptions));
+
+ ///
+ public override async Task RunAsync(TaskActivityContext context, ExportRequest input)
+ {
+ Check.NotNull(input, nameof(input));
+ Check.NotNullOrEmpty(input.InstanceId, nameof(input.InstanceId));
+ Check.NotNull(input.Destination, nameof(input.Destination));
+ Check.NotNull(input.Format, nameof(input.Format));
+
+ string instanceId = input.InstanceId;
+
+ try
+ {
+ this.logger.LogInformation("Starting export for instance {InstanceId}", instanceId);
+
+ // Get instance metadata with inputs and outputs
+ OrchestrationMetadata? metadata = await this.client.GetInstanceAsync(
+ instanceId,
+ getInputsAndOutputs: true,
+ cancellation: CancellationToken.None);
+
+ if (metadata == null)
+ {
+ string error = $"Instance {instanceId} not found";
+ this.logger.LogWarning(error);
+ return new ExportResult
+ {
+ InstanceId = instanceId,
+ Success = false,
+ Error = error,
+ };
+ }
+
+ // Get completed timestamp (LastUpdatedAt for terminal states)
+ DateTimeOffset completedTimestamp = metadata.LastUpdatedAt;
+ if (!metadata.IsCompleted)
+ {
+ string error = $"Instance {instanceId} is not in a completed state";
+ this.logger.LogWarning(error);
+ return new ExportResult
+ {
+ InstanceId = instanceId,
+ Success = false,
+ Error = error,
+ };
+ }
+
+ // Stream all history events
+ this.logger.LogInformation("Streaming history events for instance {InstanceId}", instanceId);
+ List historyEvents = new();
+ await foreach (HistoryEvent historyEvent in this.client.StreamInstanceHistoryAsync(
+ instanceId,
+ executionId: null, // Use latest execution
+ cancellation: CancellationToken.None))
+ {
+ historyEvents.Add(historyEvent);
+ }
+
+ this.logger.LogInformation(
+ "Retrieved {EventCount} history events for instance {InstanceId}",
+ historyEvents.Count,
+ instanceId);
+
+ // Create blob filename from hash of completed timestamp and instance ID
+ string blobFileName = GenerateBlobFileName(completedTimestamp, instanceId, input.Format);
+
+ // Build blob path with prefix if provided
+ string blobPath = string.IsNullOrEmpty(input.Destination.Prefix)
+ ? blobFileName
+ : $"{input.Destination.Prefix.TrimEnd('/')}/{blobFileName}";
+
+ // Serialize history events to JSON
+ string jsonContent = SerializeInstanceData(historyEvents, input.Format);
+
+ // Upload to blob storage
+ await this.UploadToBlobStorageAsync(
+ input.Destination.Container,
+ blobPath,
+ jsonContent,
+ input.Format,
+ instanceId,
+ CancellationToken.None);
+
+ this.logger.LogInformation(
+ "Successfully exported instance {InstanceId} to blob {BlobPath}",
+ instanceId,
+ blobPath);
+
+ return new ExportResult
+ {
+ InstanceId = instanceId,
+ Success = true,
+ };
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "Failed to export instance {InstanceId}", instanceId);
+ return new ExportResult
+ {
+ InstanceId = instanceId,
+ Success = false,
+ Error = ex.Message,
+ };
+ }
+ }
+
+ static string GenerateBlobFileName(DateTimeOffset completedTimestamp, string instanceId, ExportFormat format)
+ {
+ // Create hash from completed timestamp and instance ID
+ string hashInput = $"{completedTimestamp:O}|{instanceId}";
+ byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(hashInput));
+ string hash = Convert.ToHexString(hashBytes).ToLowerInvariant();
+
+ // Build filename with format extension
+ string extension = GetFileExtension(format);
+
+ return $"{hash}.{extension}";
+ }
+
+ ///
+ /// Gets the file extension for a given export format.
+ ///
+ /// The export format.
+ /// The file extension (e.g., "json", "jsonl.gz").
+ static string GetFileExtension(ExportFormat format)
+ {
+ return format.Kind switch
+ {
+ ExportFormatKind.Jsonl => "jsonl.gz", // JSONL format is compressed
+ ExportFormatKind.Json => "json", // JSON format is uncompressed
+ _ => "jsonl.gz", // Default to JSONL compressed
+ };
+ }
+
+ static string SerializeInstanceData(
+ List historyEvents,
+ ExportFormat format)
+ {
+ JsonSerializerOptions serializerOptions = new JsonSerializerOptions
+ {
+ WriteIndented = false,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ IncludeFields = true, // Include fields, not just properties (matches JsonDataConverter pattern)
+ Converters = { new JsonStringEnumConverter() }, // Serialize enums as strings
+ };
+
+ if (format.Kind == ExportFormatKind.Jsonl)
+ {
+ // JSONL format: one history event per line
+ // Serialize as object to preserve runtime type (polymorphic serialization)
+ StringBuilder jsonlBuilder = new();
+
+ foreach (HistoryEvent historyEvent in historyEvents)
+ {
+ // Serialize as object to preserve the actual derived type
+ string json = JsonSerializer.Serialize((object)historyEvent, historyEvent.GetType(), serializerOptions);
+ jsonlBuilder.AppendLine(json);
+ }
+
+ return jsonlBuilder.ToString();
+ }
+ else
+ {
+ // JSON format: array of history events
+ // Convert to object array to preserve runtime types
+ object[] eventsAsObjects = historyEvents.Cast