Skip to content

ADR-008: Co-Located DI Registries

Status

Approved - Implemented in PR #2591

Context

SyRF.Quartz is structurally a thin MassTransit Job-Service host: it runs the scheduler, persists job sagas in SQL Server, and dispatches messages across the bus. The actual domain work lives as IJobConsumer<T> implementations in the PM service.

Despite that shape, Quartz transitively referenced SyRF.Mongo.Common, SyRF.AppServices, and a heartbeat writer it never needed. The cause was SyRF.WebHostConfig.Common — a single "host bootstrap" library — owning DI wiring for concerns it doesn't own (Mongo, S3). A single file in WebHostConfig (SyrfConfigureServices.cs) imported from both SyRF.Mongo.Common and SyRF.AppServices.FileServices, dragging those ProjectReferences into every service that referenced WebHostConfig.

Symptoms:

  • SonarCloud scanned src/libs/mongo/** and src/libs/appservices/** under the syrf-quartz project because those assemblies landed in Quartz's build output.
  • Quartz's Program.cs carried a multi-line S3Settings placeholder hack so SyrfRegistry.WithDefaultConventions() could auto-map IS3PostSigner → S3PostSigner (from SyRF.SharedKernel) without failing DI validation.
  • DatabaseMetadataWriter wrote heartbeat entries to Mongo databases Quartz doesn't own, polluting PM/API's syrf_metadata document with irrelevant service entries.

Decision

Each feature library owns its own DI wiring. Hosts opt in by including the library's Lamar ServiceRegistry subclass directly from Program.cs. SyRF.WebHostConfig.Common keeps only generic host-bootstrap concerns (Kestrel, logging, OTEL, health checks, MassTransit).

Concretely:

  1. SyRF.Mongo.Common.MongoLamarRegistry — self-contained Mongo DI.
  2. Binds MongoConnectionSettings, ConnectionStrings, SqlConnectionSettings via AddOptions<T>().BindConfiguration(...).
  3. Registers MongoContext, RepositoryCache, IResumePointRepository, ChangeStreamHealthTracker, DatabaseMetadataWriter.
  4. Runs MongoUtils.EnsureLegacyGuidSerializer() in its constructor.
  5. Consumers call registry.IncludeRegistry<MongoLamarRegistry>() — nothing else.

  6. SyRF.AppServices.FileServices.FileStorageRegistry — self-contained S3/file-storage DI.

  7. Binds S3Settings via AddOptions<S3Settings>().BindConfiguration("S3Settings").
  8. Registers IAmazonS3, IS3PostSigner, IFileService.
  9. Consumers call registry.IncludeRegistry<FileStorageRegistry>().

  10. SyRF.WebHostConfig.Common no longer references SyRF.Mongo.Common or SyRF.AppServices. The three helpers AddSyrfMongoServices / AddSyrfDataServices / AddSyrfFileServices are deleted.

  11. S3 types moved from SharedKernel to AppServices. S3Settings, IS3PostSigner, S3PostSigner, and S3SignerBase now live in SyRF.AppServices.FileServices (namespace matches folder). SharedKernel is strictly smaller; services that don't reference AppServices no longer have S3 types in their loaded assemblies, and SyrfRegistry.WithDefaultConventions() can't auto-register them.

  12. IEarlyInitTask marker interface in SharedKernel lets ExecuteInitAndStartupMethods run lifecycle-ordering-sensitive init tasks first without a compile-time reference to any concrete type. DatabaseMetadataWriter stays in SyRF.Mongo.Common; a MongoInitHook adapter (also in Mongo.Common) implements IEarlyInitTask and delegates Execute() to it. The adapter pattern prevents Lamar's assembly scan from auto-registering IEarlyInitTask → DatabaseMetadataWriter by type, which would cause container validation to fail in test hosts that replace DatabaseMetadataWriter via services.AddSingleton.

Consequences

Positive

  • Quartz stops compiling Mongo/S3 assemblies. Clean build verification shows Quartz's output contains only SyRF.Quartz.dll, SyRF.SharedKernel.dll, and SyRF.WebHostConfig.Common.dll — no SyRF.Mongo.Common.dll or SyRF.AppServices.dll.
  • SonarCloud's syrf-quartz scan scope shrinks to files that Quartz actually compiles. No more coverage-exclusion workarounds for Mongo/S3 classes.
  • The S3Settings placeholder hack in Quartz Program.cs is gone. S3PostSigner is no longer in Quartz's loaded assemblies, so nothing tries to construct it.
  • Quartz no longer writes to syrf_metadata — it never had a meaningful entry there (Quartz owns no Mongo DB). Liveness / version observability continues via /health/live (K8s probes, UI version dialog), Elastic APM, Sentry, and ArgoCD deploy notifications.
  • Each feature library is a proper unit. Changing Mongo wiring touches MongoLamarRegistry; changing S3 wiring touches FileStorageRegistry. No more cross-cutting edits in a grab-bag hosting library.
  • SharedKernel is strictly smaller. S3 concerns removed; SharedKernel's role is closer to "cross-cutting domain primitives" with no hosting artifacts.

Negative

  • Host Program.cs files gain an explicit IncludeRegistry<T>() call per included registry instead of a single chainable AddSyrfDataServices(config). The explicit form is more honest about what's registered.
  • ExecuteInitAndStartupMethods depends on the IEarlyInitTask convention instead of a concrete type. The MongoInitHook adapter is a small amount of extra glue.
  • Adding a new concern that needs a registry (e.g. SQL, Redis) means creating a new ServiceRegistry subclass in the library that owns the concern — the indirection that used to live in WebHostConfig as a single method per concern now lives as a per-lib class.

Neutral

  • NuGet packages added to SyRF.AppServices: Lamar, Microsoft.Extensions.Configuration.Abstractions, Microsoft.Extensions.DependencyInjection.Abstractions, Microsoft.Extensions.Options.ConfigurationExtensions. Minor transitive weight; AppServices was already a DI-aware library in practice.

Guidelines for future work

When adding a new cross-cutting feature (e.g. Redis cache)

  1. Create SyRF.Caching.Redis (or similar) with a RedisLamarRegistry : ServiceRegistry class.
  2. Register the feature's types inside the registry. Use this.AddOptions<RedisSettings>().BindConfiguration("Redis") to bind config lazily.
  3. Consumers call registry.IncludeRegistry<RedisLamarRegistry>() in their Program.cs.
  4. Do not put the wiring in SyRF.WebHostConfig.Common.

When adding an init-order-sensitive task

  • If the task must run before regular IRunAtInit, make a separate adapter class that implements IEarlyInitTask and delegates to the real type. Register the adapter with For<IEarlyInitTask>().Add(...) using a factory.
  • Do not make the real type implement IEarlyInitTask directly — Lamar's scan will auto-register it by type and validation will try to construct it via its real constructor, breaking test hosts that mock the type.

When Quartz gains a new dependency

  • Add a direct <ProjectReference> from Quartz's .csproj. Do not rely on transitive WebHostConfig references.
  • Keep the "Quartz is a thin scheduler" contract: its persistence is SQL Server for saga state only; its observability is K8s probes + APM + Sentry + ArgoCD, not syrf_metadata.
  • PR #2591 — the implementation of this ADR.
  • docs/planning/quartz-decoupling-analysis.md — options analysis that led to this decision (A+B adopted; C and D rejected).
  • ADR-003 (Cluster Architecture) — sonar project structure follows from the mono-sonar-per-service pattern; this ADR works within that constraint.