Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
107 changes: 107 additions & 0 deletions .github/workflows/build-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ on:
- "csharp-language/csharp-12-features/**"
- "csharp-language/high-performance-memory-management/**"
- "csharp-language/modern-patterns-result-pipeline/**"
- "dotnet-8-essentials/background-jobs-hostedservice-queues/**"
- ".github/workflows/build-samples.yml"

pull_request:
Expand All @@ -46,6 +47,7 @@ on:
- "csharp-language/csharp-12-features/**"
- "csharp-language/high-performance-memory-management/**"
- "csharp-language/modern-patterns-result-pipeline/**"
- "dotnet-8-essentials/background-jobs-hostedservice-queues/**"
- ".github/workflows/build-samples.yml"

workflow_dispatch:
Expand Down Expand Up @@ -993,3 +995,108 @@ jobs:
)"

test "${line_count}" = "5"

test-background-job-queue:
name: Test bounded background job queue
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Check out repository
uses: actions/checkout@v5

- name: Install .NET 10 SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"

- name: Restore
run: >
dotnet restore
dotnet-8-essentials/background-jobs-hostedservice-queues/BackgroundJobQueueMinimal.slnx

- name: Build
run: >
dotnet build
dotnet-8-essentials/background-jobs-hostedservice-queues/BackgroundJobQueueMinimal.slnx
--configuration Release
--no-restore

- name: Test
run: >
dotnet test
dotnet-8-essentials/background-jobs-hostedservice-queues/BackgroundJobQueueMinimal.slnx
--configuration Release
--no-build

- name: Smoke-test API
shell: bash
run: |
app_log="${RUNNER_TEMP}/background-job-queue.log"

ASPNETCORE_URLS="http://127.0.0.1:5096" \
dotnet run \
--project dotnet-8-essentials/background-jobs-hostedservice-queues/src/BackgroundJobQueueMinimal/BackgroundJobQueueMinimal.csproj \
--configuration Release \
--no-build \
>"${app_log}" 2>&1 &

app_pid="$!"

cleanup() {
kill "${app_pid}" 2>/dev/null || true
wait "${app_pid}" 2>/dev/null || true
}

trap cleanup EXIT

for attempt in $(seq 1 40); do
if curl \
--fail \
--silent \
http://127.0.0.1:5096/ \
>"${RUNNER_TEMP}/background-job-root.json"; then
break
fi

if ! kill -0 "${app_pid}" 2>/dev/null; then
cat "${app_log}"
exit 1
fi

sleep 0.25
done

root="$(
cat "${RUNNER_TEMP}/background-job-root.json"
)"

expected_root='{"sample":"bounded-background-job-queue","durability":"in-memory","capacity":4}'

test "${root}" = "${expected_root}"

queue="$(
curl \
--fail \
--silent \
http://127.0.0.1:5096/jobs/queue
)"

expected_queue='{"capacity":4,"depth":0,"accepting":true}'

test "${queue}" = "${expected_queue}"

status="$(
curl \
--silent \
--output "${RUNNER_TEMP}/invalid-job.json" \
--write-out '%{http_code}' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"to":"","subject":""}' \
http://127.0.0.1:5096/jobs/email
)"

test "${status}" = "400"
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
| [`csharp-language/csharp-12-features`](csharp-language/csharp-12-features/) | Focused .NET 10 console lab locked to C# 12.0, demonstrating primary constructors with explicit properties, collection expressions and spreads, default lambda parameters, tuple-type aliases, deterministic output, and unit tests | [C# 12 Language Features: Primary Constructors, Collections & More](https://www.dotnet-guide.com/tutorials/csharp-language/csharp-12-features/) |
| [`csharp-language/high-performance-memory-management`](csharp-language/high-performance-memory-management/) | Focused .NET 10 UTF-8 ingestion sample demonstrating PipeReader framing, segmented ReadOnlySequence handling, span-based numeric parsing, bounded MemoryPool ownership, strict input validation, deterministic output, and tests | [High-Performance C#: Span<T>, Memory<T>, SIMD & Pipelines](https://www.dotnet-guide.com/tutorials/csharp-language/high-performance-memory-management/) |
| [`csharp-language/modern-patterns-result-pipeline`](csharp-language/modern-patterns-result-pipeline/) | Focused .NET 10 console companion locked to C# 12.0, demonstrating a custom Result type, safe and diagnostic errors, Map/Bind/BindAsync composition, invariant text-import validation, short-circuit persistence, cancellation, deterministic output, and tests | [C# 12 Functional Patterns: Result Type, Error Handling & Composable Pipeline Testing](https://www.dotnet-guide.com/tutorials/csharp-language/modern-patterns-result-pipeline/) |
| [`dotnet-8-essentials/background-jobs-hostedservice-queues`](dotnet-8-essentials/background-jobs-hostedservice-queues/) | Focused ASP.NET Core Minimal API demonstrating a bounded Channel queue, asynchronous backpressure, a BackgroundService consumer, fresh scoped handlers, safe in-memory job status, exception isolation, cancellation, and integration tests | [.NET 8 Background Jobs: IBackgroundTaskQueue, BackgroundService & Production-Ready Patterns](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/background-jobs-hostedservice-queues/) |

## Companion articles
- [Common Microsoft.Extensions.AI mistakes](https://www.dotnet-guide.com/articles/dotnet-ai/microsoft-extensions-ai-common-mistakes/)
Expand Down Expand Up @@ -131,6 +132,28 @@ tutorials/
| `-- TransactionalOutboxMinimal.Tests/
| |-- TransactionalOutboxMinimal.Tests.csproj
| `-- OutboxFlowTests.cs
|-- dotnet-8-essentials/
| `-- background-jobs-hostedservice-queues/
| |-- BackgroundJobQueueMinimal.slnx
| |-- README.md
| |-- src/
| | `-- BackgroundJobQueueMinimal/
| | |-- BackgroundJobQueueMinimal.csproj
| | |-- Program.cs
| | |-- Jobs/
| | | |-- BackgroundJobModels.cs
| | | |-- IBackgroundJobQueue.cs
| | | |-- BoundedBackgroundJobQueue.cs
| | | |-- IJobTracker.cs
| | | |-- InMemoryJobTracker.cs
| | | `-- QueuedEmailWorker.cs
| | `-- Services/
| | |-- IEmailJobHandler.cs
| | `-- FakeEmailJobHandler.cs
| `-- tests/
| `-- BackgroundJobQueueMinimal.Tests/
| |-- BackgroundJobQueueMinimal.Tests.csproj
| `-- BackgroundJobQueueTests.cs
|-- blazor/
| |-- create-interactive-ui-csharp-12/
| | |-- BlazorTodoMinimal.slnx
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/BackgroundJobQueueMinimal/BackgroundJobQueueMinimal.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/BackgroundJobQueueMinimal.Tests/BackgroundJobQueueMinimal.Tests.csproj" />
</Folder>
</Solution>
221 changes: 221 additions & 0 deletions dotnet-8-essentials/background-jobs-hostedservice-queues/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# Bounded Background Job Queue

A focused ASP.NET Core Minimal API companion demonstrating a bounded
`Channel<T>`, asynchronous producer backpressure, a `BackgroundService`
consumer, fresh dependency-injection scopes per job, safe in-memory status
tracking, exception isolation, and cooperative cancellation.

## Full tutorial

[.NET 8 Background Jobs: IBackgroundTaskQueue, BackgroundService & Production-Ready Patterns](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/background-jobs-hostedservice-queues/)

## Framework note

The full tutorial is written for ASP.NET Core 8.

This repository companion targets .NET 10 because that is the current DOTNET
GUIDE sample SDK.

The core APIs demonstrated here are the same hosted-service and
`System.Threading.Channels` patterns.

## Flow

```text
POST /jobs/email
-> validate
-> create typed EmailJob
-> register Queued state
-> bounded Channel<EmailJob>
-> BackgroundService
-> fresh async DI scope
-> scoped handler
-> terminal status
```

## What this sample proves

- the queue is bounded;
- producers wait asynchronously when it is full;
- jobs are consumed in FIFO order;
- one consumer processes jobs sequentially;
- every job gets a fresh dependency-injection scope;
- one failed job does not stop the next job;
- host cancellation reaches the in-flight handler;
- status responses do not expose exception details;
- queue admission closes during worker shutdown.

## Non-durability boundary

The queue and status tracker live only in process memory.

A process restart loses:

- queued jobs;
- running state;
- completed status history.

`202 Accepted` means the job entered this process's in-memory queue.

It does not mean the job is durably stored or guaranteed to finish.

Use a durable broker or job framework when work must survive restarts.

## Typed jobs

The queue stores an `EmailJob` record rather than a delegate.

This keeps job payloads inspectable and avoids implying that delegates can be
serialized into Redis or a cloud queue.

A durable implementation would still need:

- a message contract;
- serialization;
- delivery acknowledgements;
- visibility or lease handling;
- retry classification;
- poison-message storage;
- idempotency.

## Shutdown model

This sample uses cancellation-first shutdown.

When stopping begins:

1. queue admission closes;
2. the worker token is canceled;
3. the in-flight handler receives cancellation;
4. queued items can remain unprocessed.

This is not a full drain or durable recovery implementation.

## API

### Enqueue

```http
POST /jobs/email
Content-Type: application/json

{
"to": "reader@example.com",
"subject": "Queue sample"
}
```

Returns:

```text
202 Accepted
Location: /jobs/{jobId}
```

### Job status

```http
GET /jobs/{jobId}
```

### Local queue status

```http
GET /jobs/queue
```

The queue-status endpoint is for demonstration and testing.

It is not an authenticated production admin API.

### Cancellation and concurrency

A producer waiting for bounded capacity uses the HTTP request cancellation
token. If the request is canceled before the channel accepts the job, the
temporary tracker registration is removed and the cancellation continues to
the caller.

Status snapshots returned by `GET /jobs/{jobId}` are synchronized
independently from the `ConcurrentDictionary` that stores tracker entries.
Each snapshot read acquires the per-entry lock, ensuring a consistent view of
terminal state after the worker transitions the job.

## Prerequisite

- .NET 10 SDK

## Restore, build, and test

```powershell
dotnet restore `
.\BackgroundJobQueueMinimal.slnx

dotnet build `
.\BackgroundJobQueueMinimal.slnx `
--configuration Release `
--no-restore

dotnet test `
.\BackgroundJobQueueMinimal.slnx `
--configuration Release `
--no-build
```

## Run

```powershell
dotnet run `
--project .\src\BackgroundJobQueueMinimal\BackgroundJobQueueMinimal.csproj `
--configuration Release `
--no-build `
--urls http://127.0.0.1:5096
```

## Project structure

```text
BackgroundJobQueueMinimal.slnx
README.md
src/
`-- BackgroundJobQueueMinimal/
|-- BackgroundJobQueueMinimal.csproj
|-- Program.cs
|-- Jobs/
| |-- BackgroundJobModels.cs
| |-- IBackgroundJobQueue.cs
| |-- BoundedBackgroundJobQueue.cs
| |-- IJobTracker.cs
| |-- InMemoryJobTracker.cs
| `-- QueuedEmailWorker.cs
`-- Services/
|-- IEmailJobHandler.cs
`-- FakeEmailJobHandler.cs
tests/
`-- BackgroundJobQueueMinimal.Tests/
|-- BackgroundJobQueueMinimal.Tests.csproj
`-- BackgroundJobQueueTests.cs
```

## Deliberately omitted

- retries;
- Polly;
- dead-letter storage;
- schedules;
- metrics exporters;
- health probes;
- durable brokers;
- databases;
- real email delivery;
- multiple workers;
- parallel processing.

## Verification

- Target framework: .NET 10
- Application NuGet dependencies: none
- Test count: 8
- External services: none
- Queue capacity: 4 by default
- Durability: in-memory only
- Last reviewed: 2026-08-06
Loading