Skip to content

Commit c68d4dc

Browse files
authored
add export job dotnet sample (#154)
1 parent 376a33f commit c68d4dc

9 files changed

Lines changed: 589 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
8+
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Azure.Identity" Version="1.17.1" />
13+
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
14+
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<PackageReference Include="Microsoft.DurableTask.Client.AzureManaged" Version="1.22.0" />
19+
<PackageReference Include="Microsoft.DurableTask.Worker.AzureManaged" Version="1.22.0" />
20+
<PackageReference Include="Microsoft.DurableTask.ExportHistory" Version="1.22.0-preview.1" />
21+
<PackageReference Include="Microsoft.DurableTask.Generators" Version="1.0.0-preview.1" OutputItemType="Analyzer" />
22+
</ItemGroup>
23+
</Project>
24+
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
### Variables
2+
@baseUrl = http://localhost:5009
3+
@jobId = export-job-12345
4+
5+
### Create a new batch export job
6+
# @name createBatchExportJob
7+
POST {{baseUrl}}/export-jobs
8+
Content-Type: application/json
9+
10+
{
11+
"jobId": "{{jobId}}",
12+
"mode": "Batch",
13+
"completedTimeFrom": "2025-10-01T00:00:00Z",
14+
"completedTimeTo": "2025-11-06T23:59:59Z",
15+
"container": "export-history",
16+
"prefix": "batch-exports/",
17+
"maxInstancesPerBatch": 1,
18+
"runtimeStatus": []
19+
}
20+
21+
### Create a new continuous export job
22+
# @name createContinuousExportJob
23+
POST {{baseUrl}}/export-jobs
24+
Content-Type: application/json
25+
26+
{
27+
"jobId": "export-job-continuous-123",
28+
"mode": "Continuous",
29+
"completedTimeFrom": "2025-10-01T00:00:00Z",
30+
"container": "export-history",
31+
"prefix": "continuous-exports/",
32+
"maxInstancesPerBatch": 1000
33+
}
34+
35+
### Create an export job with default storage (no container specified)
36+
# @name createExportJobWithDefaultStorage
37+
POST {{baseUrl}}/export-jobs
38+
Content-Type: application/json
39+
{
40+
"jobId": "export-job-default-storage",
41+
"mode": "Batch",
42+
"completedTimeFrom": "2024-01-01T00:00:00Z",
43+
"completedTimeTo": "2024-12-31T23:59:59Z",
44+
"maxInstancesPerBatch": 100
45+
}
46+
47+
### Get a specific export job by ID
48+
# Note: This endpoint can be used to verify the export job was created and check its status
49+
# The ID in the URL should match the jobId used in create request
50+
GET {{baseUrl}}/export-jobs/{{jobId}}
51+
52+
### List all export jobs
53+
GET {{baseUrl}}/export-jobs/list
54+
55+
### List export jobs with filters
56+
### Filter by status
57+
GET {{baseUrl}}/export-jobs/list?status=Active
58+
59+
### Filter by job ID prefix
60+
GET {{baseUrl}}/export-jobs/list?jobIdPrefix=export-job-
61+
62+
### Filter by creation time range
63+
GET {{baseUrl}}/export-jobs/list?createdFrom=2024-01-01T00:00:00Z&createdTo=2024-12-31T23:59:59Z
64+
65+
### Combined filters
66+
GET {{baseUrl}}/export-jobs/list?status=Completed&jobIdPrefix=export-job-&pageSize=50
67+
68+
### Delete an export job
69+
# DELETE {{baseUrl}}/export-jobs/{{jobId}}
70+
71+
# Delete a continuous export job
72+
DELETE {{baseUrl}}/export-jobs/export-job-continuous-123
73+
74+
### Tips:
75+
# - Replace the baseUrl variable if your application runs on a different port
76+
# - The jobId variable can be changed to test different export job instances
77+
# - Export modes:
78+
# - "Batch": Exports all instances within a time range (requires completedTimeTo)
79+
# - "Continuous": Continuously exports instances from a start time (completedTimeTo must be null)
80+
# - Runtime status filters (valid values):
81+
# - "Completed": Exports only completed orchestrations
82+
# - "Failed": Exports only failed orchestrations
83+
# - "Terminated": Exports only terminated orchestrations
84+
# - Dates are in ISO 8601 format (YYYY-MM-DDThh:mm:ssZ)
85+
# - You can use the REST Client extension in VS Code to execute these requests
86+
# - The @name directive allows referencing the response in subsequent requests
87+
# - Export jobs run asynchronously; use GET to check the status after creation
88+
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.DurableTask;
6+
using Microsoft.DurableTask.Client;
7+
using Microsoft.DurableTask.ExportHistory;
8+
using ExportHistoryWebApp.Models;
9+
10+
namespace ExportHistoryWebApp.Controllers;
11+
12+
/// <summary>
13+
/// Controller for managing export history jobs through a REST API.
14+
/// Provides endpoints for creating, reading, listing, and deleting export jobs.
15+
/// </summary>
16+
[ApiController]
17+
[Route("export-jobs")]
18+
public class ExportJobController : ControllerBase
19+
{
20+
readonly ExportHistoryClient exportHistoryClient;
21+
readonly ILogger<ExportJobController> logger;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="ExportJobController"/> class.
25+
/// </summary>
26+
/// <param name="exportHistoryClient">Client for managing export history jobs.</param>
27+
/// <param name="logger">Logger for recording controller operations.</param>
28+
public ExportJobController(
29+
ExportHistoryClient exportHistoryClient,
30+
ILogger<ExportJobController> logger)
31+
{
32+
this.exportHistoryClient = exportHistoryClient ?? throw new ArgumentNullException(nameof(exportHistoryClient));
33+
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
34+
}
35+
36+
/// <summary>
37+
/// Creates a new export job based on the provided configuration.
38+
/// </summary>
39+
/// <param name="request">The export job creation request.</param>
40+
/// <returns>The created export job description.</returns>
41+
[HttpPost]
42+
public async Task<ActionResult<ExportJobDescription>> CreateExportJob([FromBody] CreateExportJobRequest request)
43+
{
44+
if (request == null)
45+
{
46+
return this.BadRequest("createExportJobRequest cannot be null");
47+
}
48+
49+
try
50+
{
51+
ExportDestination? destination = null;
52+
if (!string.IsNullOrEmpty(request.Container))
53+
{
54+
destination = new ExportDestination(request.Container)
55+
{
56+
Prefix = request.Prefix,
57+
};
58+
}
59+
60+
ExportJobCreationOptions creationOptions = new ExportJobCreationOptions(
61+
mode: request.Mode,
62+
completedTimeFrom: request.CompletedTimeFrom,
63+
completedTimeTo: request.CompletedTimeTo,
64+
destination: destination,
65+
jobId: request.JobId,
66+
format: request.Format,
67+
runtimeStatus: request.RuntimeStatus,
68+
maxInstancesPerBatch: request.MaxInstancesPerBatch);
69+
70+
ExportHistoryJobClient jobClient = await this.exportHistoryClient.CreateJobAsync(creationOptions);
71+
ExportJobDescription description = await jobClient.DescribeAsync();
72+
73+
this.logger.LogInformation("Created new export job with ID: {JobId}", description.JobId);
74+
75+
return this.CreatedAtAction(nameof(GetExportJob), new { id = description.JobId }, description);
76+
}
77+
catch (ArgumentException ex)
78+
{
79+
this.logger.LogError(ex, "Validation failed while creating export job {JobId}", request.JobId);
80+
return this.BadRequest(ex.Message);
81+
}
82+
catch (Exception ex)
83+
{
84+
this.logger.LogError(ex, "Error creating export job {JobId}", request.JobId);
85+
return this.StatusCode(500, "An error occurred while creating the export job");
86+
}
87+
}
88+
89+
/// <summary>
90+
/// Retrieves a specific export job by its ID.
91+
/// </summary>
92+
/// <param name="id">The ID of the export job to retrieve.</param>
93+
/// <returns>The export job description if found.</returns>
94+
[HttpGet("{id}")]
95+
public async Task<ActionResult<ExportJobDescription>> GetExportJob(string id)
96+
{
97+
try
98+
{
99+
ExportJobDescription? job = await this.exportHistoryClient.GetJobAsync(id);
100+
return this.Ok(job);
101+
}
102+
catch (ExportJobNotFoundException)
103+
{
104+
return this.NotFound();
105+
}
106+
catch (Exception ex)
107+
{
108+
this.logger.LogError(ex, "Error retrieving export job {JobId}", id);
109+
return this.StatusCode(500, "An error occurred while retrieving the export job");
110+
}
111+
}
112+
113+
/// <summary>
114+
/// Lists all export jobs, optionally filtered by query parameters.
115+
/// </summary>
116+
/// <param name="status">Optional filter by job status.</param>
117+
/// <param name="jobIdPrefix">Optional filter by job ID prefix.</param>
118+
/// <param name="createdFrom">Optional filter for jobs created after this time.</param>
119+
/// <param name="createdTo">Optional filter for jobs created before this time.</param>
120+
/// <param name="pageSize">Optional page size for pagination.</param>
121+
/// <param name="continuationToken">Optional continuation token for pagination.</param>
122+
/// <returns>A collection of export job descriptions.</returns>
123+
[HttpGet("list")]
124+
public async Task<ActionResult<IEnumerable<ExportJobDescription>>> ListExportJobs(
125+
[FromQuery] ExportJobStatus? status = null,
126+
[FromQuery] string? jobIdPrefix = null,
127+
[FromQuery] DateTimeOffset? createdFrom = null,
128+
[FromQuery] DateTimeOffset? createdTo = null,
129+
[FromQuery] int? pageSize = null,
130+
[FromQuery] string? continuationToken = null)
131+
{
132+
this.logger.LogInformation("GET list endpoint called with method: {Method}", this.HttpContext.Request.Method);
133+
try
134+
{
135+
ExportJobQuery? query = null;
136+
if (
137+
status.HasValue ||
138+
!string.IsNullOrEmpty(jobIdPrefix) ||
139+
createdFrom.HasValue ||
140+
createdTo.HasValue ||
141+
pageSize.HasValue ||
142+
!string.IsNullOrEmpty(continuationToken)
143+
)
144+
{
145+
query = new ExportJobQuery
146+
{
147+
Status = status,
148+
JobIdPrefix = jobIdPrefix,
149+
CreatedFrom = createdFrom,
150+
CreatedTo = createdTo,
151+
PageSize = pageSize,
152+
ContinuationToken = continuationToken,
153+
};
154+
}
155+
156+
AsyncPageable<ExportJobDescription> jobs = this.exportHistoryClient.ListJobsAsync(query);
157+
158+
// Collect all jobs from the async pageable
159+
List<ExportJobDescription> jobList = new List<ExportJobDescription>();
160+
await foreach (ExportJobDescription job in jobs)
161+
{
162+
jobList.Add(job);
163+
}
164+
165+
return this.Ok(jobList);
166+
}
167+
catch (Exception ex)
168+
{
169+
this.logger.LogError(ex, "Error retrieving export jobs");
170+
return this.StatusCode(500, "An error occurred while retrieving export jobs");
171+
}
172+
}
173+
174+
/// <summary>
175+
/// Deletes an export job by its ID.
176+
/// </summary>
177+
/// <param name="id">The ID of the export job to delete.</param>
178+
/// <returns>No content if successful.</returns>
179+
[HttpDelete("{id}")]
180+
public async Task<IActionResult> DeleteExportJob(string id)
181+
{
182+
this.logger.LogInformation("DELETE endpoint called for job ID: {JobId}", id);
183+
try
184+
{
185+
ExportHistoryJobClient jobClient = this.exportHistoryClient.GetJobClient(id);
186+
await jobClient.DeleteAsync();
187+
this.logger.LogInformation("Successfully deleted export job {JobId}", id);
188+
return this.NoContent();
189+
}
190+
catch (ExportJobNotFoundException)
191+
{
192+
this.logger.LogWarning("Export job {JobId} not found for deletion", id);
193+
return this.NotFound();
194+
}
195+
catch (Exception ex)
196+
{
197+
this.logger.LogError(ex, "Error deleting export job {JobId}", id);
198+
return this.StatusCode(500, "An error occurred while deleting the export job");
199+
}
200+
}
201+
}
202+
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.DurableTask.Client;
5+
using Microsoft.DurableTask.ExportHistory;
6+
7+
namespace ExportHistoryWebApp.Models;
8+
9+
/// <summary>
10+
/// Represents a request to create a new export job.
11+
/// </summary>
12+
public class CreateExportJobRequest
13+
{
14+
/// <summary>
15+
/// Gets or sets the unique identifier for the export job. If not provided, a GUID will be generated.
16+
/// </summary>
17+
public string? JobId { get; set; }
18+
19+
/// <summary>
20+
/// Gets or sets the export mode (Batch or Continuous).
21+
/// </summary>
22+
public ExportMode Mode { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets the start time for the export based on completion time (inclusive). Required.
26+
/// </summary>
27+
public DateTimeOffset CompletedTimeFrom { get; set; }
28+
29+
/// <summary>
30+
/// Gets or sets the end time for the export based on completion time (inclusive). Required for Batch mode, null for Continuous mode.
31+
/// </summary>
32+
public DateTimeOffset? CompletedTimeTo { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the blob container name where exported data will be stored. Optional if default storage is configured.
36+
/// </summary>
37+
public string? Container { get; set; }
38+
39+
/// <summary>
40+
/// Gets or sets an optional prefix for blob paths.
41+
/// </summary>
42+
public string? Prefix { get; set; }
43+
44+
/// <summary>
45+
/// Gets or sets the export format settings. Optional, defaults to jsonl-gzip.
46+
/// </summary>
47+
public ExportFormat? Format { get; set; }
48+
49+
/// <summary>
50+
/// Gets or sets the orchestration runtime statuses to filter by. Optional.
51+
/// Valid statuses are: Completed, Failed, Terminated.
52+
/// </summary>
53+
public List<OrchestrationRuntimeStatus>? RuntimeStatus { get; set; }
54+
55+
/// <summary>
56+
/// Gets or sets the maximum number of instances to fetch per batch. Optional, defaults to 100.
57+
/// </summary>
58+
public int? MaxInstancesPerBatch { get; set; }
59+
}
60+

0 commit comments

Comments
 (0)