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/**andsrc/libs/appservices/**under thesyrf-quartzproject because those assemblies landed in Quartz's build output. - Quartz's
Program.cscarried a multi-lineS3Settingsplaceholder hack soSyrfRegistry.WithDefaultConventions()could auto-mapIS3PostSigner → S3PostSigner(fromSyRF.SharedKernel) without failing DI validation. DatabaseMetadataWriterwrote heartbeat entries to Mongo databases Quartz doesn't own, polluting PM/API'ssyrf_metadatadocument 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:
SyRF.Mongo.Common.MongoLamarRegistry— self-contained Mongo DI.- Binds
MongoConnectionSettings,ConnectionStrings,SqlConnectionSettingsviaAddOptions<T>().BindConfiguration(...). - Registers
MongoContext,RepositoryCache,IResumePointRepository,ChangeStreamHealthTracker,DatabaseMetadataWriter. - Runs
MongoUtils.EnsureLegacyGuidSerializer()in its constructor. -
Consumers call
registry.IncludeRegistry<MongoLamarRegistry>()— nothing else. -
SyRF.AppServices.FileServices.FileStorageRegistry— self-contained S3/file-storage DI. - Binds
S3SettingsviaAddOptions<S3Settings>().BindConfiguration("S3Settings"). - Registers
IAmazonS3,IS3PostSigner,IFileService. -
Consumers call
registry.IncludeRegistry<FileStorageRegistry>(). -
SyRF.WebHostConfig.Commonno longer referencesSyRF.Mongo.CommonorSyRF.AppServices. The three helpersAddSyrfMongoServices/AddSyrfDataServices/AddSyrfFileServicesare deleted. -
S3 types moved from SharedKernel to AppServices.
S3Settings,IS3PostSigner,S3PostSigner, andS3SignerBasenow live inSyRF.AppServices.FileServices(namespace matches folder). SharedKernel is strictly smaller; services that don't reference AppServices no longer have S3 types in their loaded assemblies, andSyrfRegistry.WithDefaultConventions()can't auto-register them. -
IEarlyInitTaskmarker interface in SharedKernel letsExecuteInitAndStartupMethodsrun lifecycle-ordering-sensitive init tasks first without a compile-time reference to any concrete type.DatabaseMetadataWriterstays inSyRF.Mongo.Common; aMongoInitHookadapter (also in Mongo.Common) implementsIEarlyInitTaskand delegatesExecute()to it. The adapter pattern prevents Lamar's assembly scan from auto-registeringIEarlyInitTask → DatabaseMetadataWriterby type, which would cause container validation to fail in test hosts that replaceDatabaseMetadataWriterviaservices.AddSingleton.
Consequences¶
Positive¶
- Quartz stops compiling Mongo/S3 assemblies. Clean build verification shows Quartz's output contains only
SyRF.Quartz.dll,SyRF.SharedKernel.dll, andSyRF.WebHostConfig.Common.dll— noSyRF.Mongo.Common.dllorSyRF.AppServices.dll. - SonarCloud's
syrf-quartzscan scope shrinks to files that Quartz actually compiles. No more coverage-exclusion workarounds for Mongo/S3 classes. - The
S3Settingsplaceholder hack in QuartzProgram.csis gone.S3PostSigneris 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 touchesFileStorageRegistry. 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.csfiles gain an explicitIncludeRegistry<T>()call per included registry instead of a single chainableAddSyrfDataServices(config). The explicit form is more honest about what's registered. ExecuteInitAndStartupMethodsdepends on theIEarlyInitTaskconvention 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
ServiceRegistrysubclass 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)¶
- Create
SyRF.Caching.Redis(or similar) with aRedisLamarRegistry : ServiceRegistryclass. - Register the feature's types inside the registry. Use
this.AddOptions<RedisSettings>().BindConfiguration("Redis")to bind config lazily. - Consumers call
registry.IncludeRegistry<RedisLamarRegistry>()in theirProgram.cs. - 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 implementsIEarlyInitTaskand delegates to the real type. Register the adapter withFor<IEarlyInitTask>().Add(...)using a factory. - Do not make the real type implement
IEarlyInitTaskdirectly — 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.
Related¶
- 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.