Substantive.Infrastructure.ContainerJobRunner 9.17.34-build20260914r1
Substantive.Infrastructure.ContainerJobRunner
A .NET library that lets a host application trigger a standalone Worker Service across different deployment targets without changing the calling code. The deployment mode is selected at call time via an enum, and the library routes to the correct backend.
Why the name ContainerJobRunner
The library runs a container job — one unit of work that runs to completion and then exits — with an Azure Container Apps (ACA) Job as the primary production target. The name is built from those three parts: Container + Job + Runner.
Jobcaptures the run-once-and-exit semantics across all modes — local process, local Docker, remote image, and ACA Job alike.Containerreflects the primary substrate: in production every job runs as a container (an ACA Job). TheDotNetWorkerServiceProjectmode, which runs the job as a baredotnetOS process, is a development/debug convenience — a fast inner-loop stand-in for the same container job.Runneris the action the library performs: it runs the job and returns immediately (fire-and-forget).
The name lines up with the type surface callers use: IContainerJobRunnerFacade, IContainerJobRunner, ContainerJobRunnerOptions, and AddContainerJobRunner().
Purpose and mental model
A Worker Service is a self-contained .NET process or container that starts, does one unit of work, and exits. The runner is the bridge between a host app (a WebAPI, console app, or another ACA Container App) and the worker's deployment boundary.
The host app calls IContainerJobRunnerFacade.RunAsync(jobName, mode, payload) and the library handles everything else: creating or starting the process / container / ACA Job, injecting the payload, and returning immediately (fire-and-forget). The host never manages the worker's lifecycle directly.
Deploy modes
WorkerDeploymentMode enum
├─ DotNetWorkerServiceProject (10) — launches worker .dll as a local OS process via dotnet
├─ LocalContainer (11) — builds Docker image from Dockerfile, runs with --rm
├─ LocalContainerImage (12) — runs a locally cached image; builds+caches if missing
├─ RemoteContainerImage (13) — docker pull from registry, then docker run --rm
└─ AcaJob (21) — Azure Container Apps Job (production mode)
The first four are for development and testing. AcaJob is the production mode.
How AcaJob mode works (production scenario)
This is the primary production mode. The host application (e.g. Stub.Api) runs inside Azure Container Apps as a regular Container App. When it needs to run background work, it calls the runner with WorkerDeploymentMode.AcaJob.
Execution flow:
Stub.Api (ACA Container App)
└─ IContainerJobRunnerFacade.RunAsync("MyWorker", AcaJob, payload)
└─ AcaJobRunner
1. GET /subscriptions/.../jobs/{jobName}
├─ 200 OK → job already exists, skip provisioning
└─ 404 → CreateJobAsync()
├─ derives location from AcaEnvironmentId
├─ creates ManualTrigger job (10-minute replica timeout)
├─ registers registry credentials (or skips for Managed Identity pull)
└─ sets container image + 0.5 vCPU / 1 Gi
2. jobResource.StartAsync(WaitUntil.Started, executionTemplate)
└─ injects WORKER_PAYLOAD env var into the execution
3. returns immediately (fire-and-forget)
└─ ACA runs the container to completion, then auto-destroys it
What persists vs what is destroyed:
| Artifact | Lifecycle |
|---|---|
ACA Job ARM resource (Microsoft.App/jobs/{name}) |
Created once on first call; reused on every subsequent call |
| Job execution (container instance) | Started per call; ACA destroys the container automatically when it exits |
| Execution history record | Retained in ACA (visible in Azure Portal under Execution History) |
The "destroy the container" expectation is correct: ACA auto-destroys the container instance after the worker exits. The job definition template stays in Azure and is reused.
On-the-fly job provisioning
If the ACA Job ARM resource identified by JobResourceId does not yet exist, the runner provisions it automatically using:
AcaEnvironmentId— which Container Apps Environment to deploy into (used to derive the Azure region)ContainerImage— the registry image for the workerRegistryServer/RegistryUsername/RegistryPassword— only needed when not using Managed Identity for ACR pull
Once created, the job ARM resource is never deleted by the runner. Subsequent calls skip provisioning.
Required configuration for on-the-fly provisioning:
"ContainerJobRunnerOptions": {
"Workers": [
{
"JobName": "MyWorker",
"JobResourceId": "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.App/jobs/{name}",
"AcaEnvironmentId": "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.App/managedEnvironments/{env}",
"ContainerImage": "myregistry.azurecr.io/my-worker:1.0.0"
}
]
}
If the job already exists in Azure, only JobResourceId is required.
Payload delivery
The payload is serialised as JSON and injected into every invocation target via the WORKER_PAYLOAD environment variable — identical across all five modes.
var payload = new JobPayload(
JobId: Guid.CreateVersion7().ToString(),
Parameters: new Dictionary<string, string>
{
["customerId"] = request.CustomerId,
["reportDate"] = request.ReportDate.ToString("O"),
});
await factory.RunAsync("MyWorker", WorkerDeploymentMode.AcaJob, payload, ct);
The Worker Service reads the payload:
var json = Environment.GetEnvironmentVariable("WORKER_PAYLOAD")
?? throw new InvalidOperationException("WORKER_PAYLOAD env var not set.");
var payload = JsonSerializer.Deserialize<JobPayload>(json)!;
Authentication
The library registers ArmClient with DefaultAzureCredential:
- Local development — uses the signed-in Azure CLI identity (
az login) - Production (ACA) — uses the host app's Managed Identity
For ACA Job provisioning and execution, the host app's identity needs at minimum:
Azure Container Apps Contributorrole on the resource group (or the job resource itself)AcrPullrole on the container registry when using Managed Identity for image pull
If registry credentials are set in configuration (RegistryServer, RegistryUsername, RegistryPassword), Managed Identity pull is skipped and the provided credentials are used instead.
DI registration
// Program.cs (in the host app — e.g. Stub.Api)
ContainerJobRunnerOptions options = new();
builder.Configuration.GetSection(nameof(ContainerJobRunnerOptions)).Bind(options);
builder.Services.AddContainerJobRunner(options.ToAction());
AddContainerJobRunner registers all five runner implementations and IContainerJobRunnerFacade. Validation runs at host startup — a misconfigured worker fails fast with a clear error message.
Inject and use:
public class ReportController(IContainerJobRunnerFacade containerJobRunner)
{
[HttpPost("reports")]
public async Task<IActionResult> GenerateAsync(GenerateReportRequest request, CancellationToken ct)
{
var payload = new JobPayload(
JobId: Guid.CreateVersion7().ToString(),
Parameters: new Dictionary<string, string> { ["reportId"] = request.ReportId });
await containerJobRunner.RunAsync("ReportWorker", WorkerDeploymentMode.AcaJob, payload, ct);
return Accepted(new { payload.JobId });
}
}
Configuration shape reference
WorkerDefinition uses flat properties. The composition sub-objects (AcaJob, DotNetProcess, etc.) exist for validation but the runners read from the flat properties directly.
| Property | Deploy mode(s) | Required |
|---|---|---|
JobName |
all | yes |
JobResourceId |
AcaJob | yes |
AcaEnvironmentId |
AcaJob | only for on-the-fly provisioning |
ContainerImage |
AcaJob | only for on-the-fly provisioning |
RegistryServer / RegistryUsername / RegistryPassword |
AcaJob, RemoteContainerImage | only when not using Managed Identity |
AssemblyPath |
DotNetWorkerServiceProject | yes |
DockerfilePath / BuildContext |
LocalContainer | yes |
ImageName |
LocalContainerImage, RemoteContainerImage | yes |
EnableDebugPort / DebugPort |
LocalContainer, LocalContainerImage, RemoteContainerImage | optional |
Worker Service implementation contract
The worker must:
- Read
WORKER_PAYLOADfrom the environment and deserialise it asJobPayload. - Perform its work.
- Exit with code 0 on success (ACA marks the execution
Succeeded) or non-zero on failure (ACA marks itFailedand may retry perreplicaRetryLimit). - For self-hosting: call
lifetime.StopApplication()at the end ofExecuteAsyncto trigger a clean Generic Host shutdown.
Observability
For AcaJob mode, the runner logs the execution name on start:
ACA Job execution started. ExecutionName=myjob--abc123 ExecutionId=... JobId=...
To verify worker logs, query Log Analytics: ContainerAppConsoleLogs_CL | where ContainerGroupName_s startswith 'myjob--abc123' | project TimeGenerated, Log_s
The execution name is the short ARM resource name derived from the execution ID. Use it to filter Log Analytics output directly in Azure Portal or via az containerapp job execution logs show.
Key design decisions
- Fire-and-forget:
AcaJobmode usesWaitUntil.Started, so the runner returns as soon as Azure accepts the start request. The host app's HTTP request is not held open while the worker runs. Work outcome must be tracked separately (database, correlation ID, Log Analytics query). - Single façade, multiple runners: The caller only injects
IContainerJobRunnerFacade. It never talks to runner implementations directly. - Fail-fast validation:
ContainerJobRunnerFacadevalidates required properties for the chosen mode before delegating. Missing config throwsInvalidOperationExceptionwith a clear message. - ArmClient reuse: Registered as
TryAddSingleton, so the host app's existingArmClient(if any) is used rather than creating a duplicate.
Showing the top 20 packages that depend on Substantive.Infrastructure.ContainerJobRunner.
| Packages | Downloads |
|---|---|
|
Substantive.Metapackage.Backends
A metapackage contains all Substantive backend building blocks.
|
1 |
|
Substantive.Metapackage.Backends
A metapackage contains all Substantive backend building blocks.
|
2 |
|
Substantive.Metapackage.Backends
A metapackage contains all Substantive backend building blocks.
|
3 |
|
Substantive.Metapackage.Backends
A metapackage contains all Substantive backend building blocks.
|
4 |
.NET 10.0
- Azure.Identity (>= 1.21.0)
- Azure.ResourceManager.AppContainers (>= 1.5.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.12)
| Version | Downloads | Last updated |
|---|---|---|
| 9.17.34-build20260919R3 | 1 | 09/19/2026 |
| 9.17.34-build20260919R2 | 1 | 09/19/2026 |
| 9.17.34-build20260914R1 | 2 | 09/14/2026 |
| 9.17.33-build20260715R1 | 8 | 07/15/2026 |
| 9.17.32-build20260701R1 | 8 | 07/01/2026 |