Current state
Middleware execution order is explicit via int Order on IIngestionMiddleware, with an IngestionOrder constants class defining canonical positions (Logging=0, ContentHashing=10, IdempotencyCheck=20, ExifExtraction=30, Persistence=40). IngestionPipelineRunner sorts by Order at construction time and throws InvalidOperationException on duplicate orders — misconfiguration fails at DI scope creation, not mid-file.
CanInvoke / OnCannotInvoke / PipelineConfigurationException were removed as part of implementing this. The positional/semantic ordering concern from the original design (logging must be outermost, idempotency before EXIF) is fully addressed by the Order constants.
Remaining enhancement (post-1.0)
As the pipeline grows, manually assigning integer slots becomes awkward — especially for middleware added by contributors who shouldn't need to know the global order table. A data-driven dependency graph would let each middleware declare what it produces/needs and have the runner derive the order automatically.
Each IIngestionMiddleware would declare:
internal interface IIngestionMiddleware
{
// ...existing members...
/// Context properties this middleware expects to be populated before it runs.
IReadOnlySet<string> Needs => [];
/// Context properties this middleware guarantees to populate after it runs.
IReadOnlySet<string> Produces => [];
}
IngestionPipelineRunner.BuildValidated would perform a topological sort and throw a descriptive error at startup for:
- An unsatisfied dependency (a
Needs entry with no matching Produces).
- A circular dependency.
The int Order property would remain as a tie-breaker for pairs that share no data dependency (e.g. IdempotencyCheck vs ExifExtraction — both need ContentHash but neither produces what the other needs; Order keeps IdempotencyCheck earlier to avoid reading EXIF for an already-ingested file). LoggingMiddleware has no data deps either and stays outermost via Order=0.
Example declarations:
ContentHashingMiddleware produces: { ContentHash }
IdempotencyCheckMiddleware needs: { ContentHash }
ExifExtractionMiddleware needs: { ContentHash } produces: { Exif }
PersistenceMiddleware needs: { ContentHash, Exif }
LoggingMiddleware (no data deps; Order=0 keeps it outermost)
Acceptance criteria
Current state
Middleware execution order is explicit via
int OrderonIIngestionMiddleware, with anIngestionOrderconstants class defining canonical positions (Logging=0,ContentHashing=10,IdempotencyCheck=20,ExifExtraction=30,Persistence=40).IngestionPipelineRunnersorts byOrderat construction time and throwsInvalidOperationExceptionon duplicate orders — misconfiguration fails at DI scope creation, not mid-file.CanInvoke/OnCannotInvoke/PipelineConfigurationExceptionwere removed as part of implementing this. The positional/semantic ordering concern from the original design (logging must be outermost, idempotency before EXIF) is fully addressed by theOrderconstants.Remaining enhancement (post-1.0)
As the pipeline grows, manually assigning integer slots becomes awkward — especially for middleware added by contributors who shouldn't need to know the global order table. A data-driven dependency graph would let each middleware declare what it produces/needs and have the runner derive the order automatically.
Each
IIngestionMiddlewarewould declare:IngestionPipelineRunner.BuildValidatedwould perform a topological sort and throw a descriptive error at startup for:Needsentry with no matchingProduces).The
int Orderproperty would remain as a tie-breaker for pairs that share no data dependency (e.g.IdempotencyCheckvsExifExtraction— both needContentHashbut neither produces what the other needs;OrderkeepsIdempotencyCheckearlier to avoid reading EXIF for an already-ingested file).LoggingMiddlewarehas no data deps either and stays outermost viaOrder=0.Example declarations:
Acceptance criteria
IIngestionMiddlewareexposesNeedsandProduceswith empty-set defaults (no breaking change for existing middlewares).IngestionPipelineRunnerderives execution order from the declared dependency graph (topological sort), usingint Orderas a tie-breaker.