Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
34 changes: 24 additions & 10 deletions Microsoft.DurableTask.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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}
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>

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

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");
}
}

/// <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