⚙ Ingestion — engineering reference ↗ UX guide
Wholesale · Deliveries · internals

Inbound Delivery Ingestion — engineering reference

How the asynchronous ingestion pipeline is wired for engineers: the entry point and every step, with the services it uses and the concrete resources it touches — database tables, SQL stored procedures, blob & PVC storage, Redis keys, SignalR, Hangfire and domain events.

CQRS · MediatRHangfire (Redis)EF Core · SQL Server Redis (StackExchange)SignalRAzure BlobPVC file storageMassTransit · RabbitMQ

1 · High-level overview

The pipeline is asynchronous and choreographed: each step is a Hangfire background command that runs in its own transaction, records its outcome, pushes live progress over SignalR, and enqueues the next command. There is no central orchestrator — the chain advances itself, and a business failure simply stops it.

Every step inherits InboundDeliveryIngestionStepHandler<TCommand>, which runs the same lifecycle around the step's own ExecuteAsync:

  1. Load the aggregate — GetWithStepsAsync (with steps + creator).
  2. Stale guard — drop if superseded attempt / paused / terminal.
  3. Stop checkHaltIfStopRequestedAsync reads the Redis stop flag; persists Stopped if set.
  4. Idempotency — skip if this step+attempt already recorded.
  5. Publish running — Redis snapshot + SignalR broadcast.
  6. ExecuteAsync — the step's real work (below).
  7. Classify outcomeSuccess / Skipped / BusinessFailure / NoOp.
  8. Record + saveRecordStep* + SaveChangesAsync (dispatches domain events).
  9. Publish the new snapshot, then EnqueueNext (unless failed / terminal).

Each command runs under TransactionResultCommandBehavior (ReadCommitted, 300 s). Transient exceptions propagate and Hangfire retries (10×); a business failure records the step as failed and stops the chain (no next command).

2 · Flow & lifecycle

Two views of the same machine: the control flow of commands and events, and the status the aggregate moves through.

Command & event choreography

command enqueued (Hangfire) · delayed schedule (grace) · domain / integration event. Each step is its own background transaction.

command event handler state
1 · create & queue one transaction
clientPOST submit send CreateInboundDeliveryIngestionCommand create ingestion→ queued post-commit …QueuedIntegrationEvent enqueue StartPipelineWhenIngestionQueuedEventHandler
2 · pipeline Hangfire · each step its own transaction
1parse 2unpack 3partner 4products 5generate keys 6reserve keys 7detect dupes 8upload keys 9await grace after grace 10create
Every step's handler records the step → pushes progress (SignalR + Redis) → enqueues the next command. A business failure stops the chain.
3 · grace window pause-able · nothing created yet
AwaitGracePeriodStepHandler schedule CreateInboundDeliveryStepCommandcarries grace token
stop → Stopped · resume → fresh token + reschedule · the converge runs only if status is still in-grace and the token matches.
4 · converge step 10 · one transaction
CreateInboundDeliveryStepCommand handler InboundDelivery.Create()
InboundDeliveryCreatedInternalEvent SupplyInboundOrdersEventHandler if overflow inbound ordergenerated
InboundDeliveryCompletedInternalEvent PickTower loadPT_Load_InboundDelivery
ingestion→ succeeded / IBO generated
The converge reads the generated order synchronously (the scoped sink populated by SupplyInboundOrders in the same save) and records it inline (→ IBO generated) — no follow-up command, no Succeeded→IboGenerated flash.
5 · generated order → success
ingestionIBO generated user fills it InboundOrderCompletedIntegrationEvent SucceedIngestion…Handler ingestion→ succeeded

Ingestion status lifecycle

The aggregate's status and what drives each transition. Dashed blue = user re-entry.

Ingestion status state machine Queued advances to Running, then In grace, then Succeeded; an overflow order routes through IBO generated; Running can branch to Failed or Stopped; re-entry commands return Stopped or Failed to Running or In grace. first step grace · step 9 converge · no order converge · order order completed business fail restart / start stop start pause resume Queued Running In grace Succeeded IBO generated Failed Stopped

start resumes the same attempt · restart begins a new attempt · resume re-arms the grace window. Failed and Stopped are deletable. Internally the converge sets Succeeded first, then capture flips it to IBO generated when an order was produced.

3 · Triggers & entry points

Everything that can move an ingestion forward.

4 · Shared infrastructure

Referenced by every step — listed once here rather than repeated below.

PVC Ingestion storage

  • IInboundDeliveryIngestionStorage at AppSettings.InboundDeliveryStoragePrefix.
  • Layout: {prefix}/ingestions/{id}/attempt-{n}/{n}.raw, parsed.json, preparedkeys.json.
  • Uploaded / extracted key files: flat temp files under {prefix}.

Redis db 1 keys

  • …:ibd-ingestion:{id} — progress snapshot (24 h).
  • …:ibd-ingestion-stop:{id} — stop flag (6 h).
  • …:ibd-key-reservation:{hash} + …-owned:{id} — key reservations (Lua, all-or-nothing, 10 min).
  • lock:ibd-ingestion-upload:{id} — upload lock (30 s).

SignalR Live progress

  • IIngestionProgressService writes the Redis snapshot and broadcasts InboundDeliveryIngestionUpdated on WholeSaleHub (/notifications) via INotificationService.BroadcastToAll.
  • Steps that report own progress call PublishStepProgressAsync for per-item counts.

Hangfire Background

  • IBackgroundService.Enqueue (next step) / Schedule (delayed converge), routed by MediatRHangfireBridge over Redis.
  • Retry policy: 10×; permanent failure → HangfireFailedJobNotification email.

Systems map

What a running step touches. The handler is a Hangfire worker; everything else is a backing system it reads from or writes to.

Ingestion systems map The ingestion step handler reads and writes SQL Server, Redis, Azure Blob, PVC file storage, publishes to SignalR and RabbitMQ, and is driven by Hangfire. EF Core · tables + SPs cache · reservations · locks enqueue / schedule key files integration events live progress artifacts + temp SQL Server · EF Core deliveries.InboundDeliveryIngestion · .Key contactBook.Partner · products.Products SP PT_CheckDuplicates_Keys · PT_Load_… SeedWork.Counter (sequence) Redis · db 1 ibd-ingestion (snapshot, 24h) ibd-ingestion-stop (6h) ibd-key-reservation (Lua, 10min) lock:ibd-ingestion-upload (30s) Hangfire · Redis enqueue next · schedule converge Azure Blob container 'keys' · file keys + MD5 MassTransit · RabbitMQ integration events (post-commit) …QueuedIntegrationEvent InboundOrderCompletedIntegrationEvent SignalR → clients WholeSaleHub /notifications …IngestionUpdated PVC · file storage ingestions/{id}/attempt-{n}/ 1.raw · parsed.json · preparedkeys.json + temp files · keys mirror Ingestion step handler Hangfire worker · 1 txn / step

5 · Entry point & the ten steps

Per step: what it does, the services it depends on, the concrete things it calls, the artifacts it reads/writes, the events it raises, and how it can fail.

DBSQL Server table SQLstored proc / sequence Redis BlobAzure PVCfile storage SignalR Hangfire Eventdomain / integration

6 · Re-entry commands

Stop / resume / start / restart / delete — and how each touches state vs. cache.

Stop / pause

POST {id}/stop

While running, sets the Redis stop flag only — no aggregate write, zero concurrency risk; the next step's start check catches it and persists Stopped. During the grace window (no step running) it persists Stopped synchronously via PauseGracePeriod (sole writer).

Resume

POST {id}/resume · grace pause only

Clears the stop flag, calls ResumeGracePeriod (fresh GraceToken, same attempt, prepared keys reused), and Schedules a new converge. The old scheduled converge no-ops on the token guard.

Start

POST {id}/start · stopped / failed

Clears the stop flag and calls Start() (→ Queued, same attempt) — the pipeline resumes from where it paused; enqueued via the queued event. Hidden for grace-paused (use Resume).

Restart

POST {id}/restart · any non-succeeded

Clears the stop flag and calls RestartFromBeginning — bumps AttemptNumber and re-runs from step 1; the prior attempt is kept as history.

Delete

DELETE {id} · stopped / failed

Removes the aggregate, clears the cache + broadcasts InboundDeliveryIngestionRemoved, releases any Redis key reservations, and sweeps the PVC attempt folder.

Finalize (generated order)

inline + integration event

The converge records any generated IBO inline (id read back from the scoped IGeneratedInboundOrderSink in the same transaction) and moves the ingestion to IBO generated — no follow-up command. When the user completes that order, InboundOrderCompletedIntegrationEventSucceedIngestionWhenGeneratedOrderCompletedEventHandlerSucceeded.