Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
871eefe
export job
YunchuWang Oct 29, 2025
c0b946b
Merge branch 'main' into wangbill/exportfinal
YunchuWang Nov 5, 2025
13c0dcb
get history events
YunchuWang Nov 5, 2025
52bf21c
v1
YunchuWang Nov 5, 2025
30fcebc
Merge branch 'main' into wangbill/exportfinal
YunchuWang Nov 7, 2025
ced2c4b
revert
YunchuWang Nov 7, 2025
5749aeb
Merge branch 'wangbill/exportfinal' of https://github.com/microsoft/d…
YunchuWang Nov 7, 2025
46b01e4
warning fix
YunchuWang Nov 7, 2025
2df6b53
unit tests
YunchuWang Nov 7, 2025
208aade
feedback
YunchuWang Nov 18, 2025
00ee88d
Merge branch 'main' into wangbill/exportfinal
YunchuWang Nov 19, 2025
b45b151
Merge branch 'main' into wangbill/exportfinal
YunchuWang Nov 25, 2025
b49b038
reset to main
YunchuWang Dec 29, 2025
350113d
add export history projects
YunchuWang Dec 29, 2025
0ad9b21
revert main
YunchuWang Dec 29, 2025
55f250e
add list
YunchuWang Dec 29, 2025
1a99521
add grpclient
YunchuWang Dec 29, 2025
ffa3219
cleanup
YunchuWang Dec 29, 2025
44252eb
Merge branch 'main' into wangbill/exportfinal
YunchuWang Dec 29, 2025
0012b53
compile fix
YunchuWang Dec 29, 2025
27d80b6
comment clear
YunchuWang Dec 29, 2025
da4bdac
test fix
YunchuWang Dec 30, 2025
6c15983
test fix
YunchuWang Dec 30, 2025
3c36aa0
Update src/ExportHistory/Exception/ExportJobNotFoundException.cs
YunchuWang Dec 30, 2025
4cedf68
Update src/ExportHistory/Activities/ExportInstanceHistoryActivity.cs
YunchuWang Dec 30, 2025
fea2e63
Update samples/ExportHistoryWebApp/ExportHistoryWebApp.http
YunchuWang Dec 30, 2025
fb8e2db
Merge branch 'main' into wangbill/exportfinal
YunchuWang Jan 5, 2026
8e7f4fa
update sample + targetframework
YunchuWang Jan 8, 2026
f9d942c
Merge branch 'main' into wangbill/exportfinal
YunchuWang Jan 8, 2026
519d239
change exportjobstatus from uninit to pending
YunchuWang Jan 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Microsoft.DurableTask.sln
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost.Tests", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsSample", "samples\EventsSample\EventsSample.csproj", "{34A3EC44-2609-A058-ED30-2F81C3F3A885}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportHistoryWebApp", "samples\ExportHistoryWebApp\ExportHistoryWebApp.csproj", "{FE1E17DD-595A-123A-EA4C-AA313BBFB685}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportHistory", "src\ExportHistory\ExportHistory.csproj", "{354CE69B-78DB-9B29-C67E-0DBB862C7A65}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportHistory.Tests", "test\ExportHistory.Tests\ExportHistory.Tests.csproj", "{05C9EBA6-7221-D458-47D6-DA457C2F893B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -277,6 +283,18 @@ Global
{34A3EC44-2609-A058-ED30-2F81C3F3A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
{34A3EC44-2609-A058-ED30-2F81C3F3A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
{34A3EC44-2609-A058-ED30-2F81C3F3A885}.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
{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
{05C9EBA6-7221-D458-47D6-DA457C2F893B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05C9EBA6-7221-D458-47D6-DA457C2F893B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05C9EBA6-7221-D458-47D6-DA457C2F893B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05C9EBA6-7221-D458-47D6-DA457C2F893B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -328,6 +346,9 @@ Global
{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
{B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
{34A3EC44-2609-A058-ED30-2F81C3F3A885} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{FE1E17DD-595A-123A-EA4C-AA313BBFB685} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{354CE69B-78DB-9B29-C67E-0DBB862C7A65} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
{05C9EBA6-7221-D458-47D6-DA457C2F893B} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71}
Expand Down
23 changes: 23 additions & 0 deletions samples/ExportHistoryWebApp/ExportHistoryWebApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Microsoft.DurableTask.Generators" OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Client\AzureManaged\Client.AzureManaged.csproj" />
<ProjectReference Include="..\..\src\Worker\AzureManaged\Worker.AzureManaged.csproj" />
<ProjectReference Include="..\..\src\ExportHistory\ExportHistory.csproj" />
</ItemGroup>
</Project>

88 changes: 88 additions & 0 deletions samples/ExportHistoryWebApp/ExportHistoryWebApp.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
### 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
# - 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

202 changes: 202 additions & 0 deletions samples/ExportHistoryWebApp/ExportJobController.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for managing export history jobs through a REST API.
/// Provides endpoints for creating, reading, listing, and deleting export jobs.
/// </summary>
[ApiController]
[Route("export-jobs")]
public class ExportJobController : ControllerBase
{
readonly ExportHistoryClient exportHistoryClient;
readonly ILogger<ExportJobController> logger;

/// <summary>
/// Initializes a new instance of the <see cref="ExportJobController"/> class.
/// </summary>
/// <param name="exportHistoryClient">Client for managing export history jobs.</param>
/// <param name="logger">Logger for recording controller operations.</param>
public ExportJobController(
ExportHistoryClient exportHistoryClient,
ILogger<ExportJobController> logger)
{
this.exportHistoryClient = exportHistoryClient ?? throw new ArgumentNullException(nameof(exportHistoryClient));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

/// <summary>
/// Creates a new export job based on the provided configuration.
/// </summary>
/// <param name="request">The export job creation request.</param>
/// <returns>The created export job description.</returns>
[HttpPost]
public async Task<ActionResult<ExportJobDescription>> 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");
}
}

/// <summary>
/// Retrieves a specific export job by its ID.
/// </summary>
/// <param name="id">The ID of the export job to retrieve.</param>
/// <returns>The export job description if found.</returns>
[HttpGet("{id}")]
public async Task<ActionResult<ExportJobDescription>> 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");
}
Comment on lines +106 to +110
}

/// <summary>
/// Lists all export jobs, optionally filtered by query parameters.
/// </summary>
/// <param name="status">Optional filter by job status.</param>
/// <param name="jobIdPrefix">Optional filter by job ID prefix.</param>
/// <param name="createdFrom">Optional filter for jobs created after this time.</param>
/// <param name="createdTo">Optional filter for jobs created before this time.</param>
/// <param name="pageSize">Optional page size for pagination.</param>
/// <param name="continuationToken">Optional continuation token for pagination.</param>
/// <returns>A collection of export job descriptions.</returns>
[HttpGet("list")]
public async Task<ActionResult<IEnumerable<ExportJobDescription>>> 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<ExportJobDescription> jobs = this.exportHistoryClient.ListJobsAsync(query);

// Collect all jobs from the async pageable
List<ExportJobDescription> jobList = new List<ExportJobDescription>();
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");
}
}

/// <summary>
/// Deletes an export job by its ID.
/// </summary>
/// <param name="id">The ID of the export job to delete.</param>
/// <returns>No content if successful.</returns>
[HttpDelete("{id}")]
public async Task<IActionResult> 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");
}
}
}

Loading
Loading