Summary
The Recipes and Plc subsystems depend on each other, so neither can be understood, tested, or replaced alone, and the DI extension methods no longer describe module boundaries.
The cycle
Recipes/RecipeSession.cs:28, 624-630 — RecipeSession (Recipes) depends on IPlcSyncService (Plc) and pushes every accepted snapshot into PLC sync.
Plc/PlcLifecycleManager.cs:23, 229 — PlcLifecycleManager (Plc) depends back on RecipeSession and drives its LoadAsCurrent* methods.
Recipes/RecipeDi.cs:21 — AddRecipe() registers PlcLifecycleManager (a Plc class), cementing the cycle at registration level; enabling "Recipe" silently pulls the whole Plc graph.
Related defects on the same boundary
- Callback re-injection (property injection in disguise).
PlcLifecycleManager.cs:208-225: RegisterReconnectApplyCallback(Func<Recipe, Task<Result>>) is a mutable register-exactly-once setter that the UI must call (RecipeCoordinator.cs:121); its doc says the callback "owns the UI-thread marshalling" — Core control flow designed around a UI concern. Violates the project's constructor-DI-only rule and creates a hidden call-order constraint with Initialize().
- Two-phase init.
PlcLifecycleManager.cs:67-77: a separate Initialize() must be called before the object works; forgetting it silently disables connection-state handling.
IPlcSyncService violates ISP and sits on the producer side. Plc/IPlcSyncService.cs:8-29: RecipeSession uses only IsSyncEnabled + NotifyRecipeChanged; the lifecycle/control surface (SetSyncEnabled, ResetForDisable, HandleConnectionLost, UpdateConnectionState, observables) is used only by PlcLifecycleManager. Project convention says interfaces belong on the consumer side.
Suggested fix
Small, mechanical inversions; dependency then flows one way (Plc → Recipes):
RecipeSession: replace the IPlcSyncService dependency with an event/IObservable<(Recipe, bool IsValid)> RecipeChanged; the Plc side subscribes.
PlcLifecycleManager: replace RegisterReconnectApplyCallback with an event/IObservable<Recipe> ReconnectRecipeAvailable — symmetric with the existing PlcRecipeConflictDetected. Fold Initialize() into construction or EnableSync.
- Split
IPlcSyncService: a narrow IRecipeSyncNotifier defined next to RecipeSession (or drop it entirely once the event exists); keep the control surface internal to Plc.
- Move
PlcLifecycleManager registration from RecipeDi into S7Di (or a dedicated AddPlc()).
Summary
The Recipes and Plc subsystems depend on each other, so neither can be understood, tested, or replaced alone, and the DI extension methods no longer describe module boundaries.
The cycle
Recipes/RecipeSession.cs:28, 624-630—RecipeSession(Recipes) depends onIPlcSyncService(Plc) and pushes every accepted snapshot into PLC sync.Plc/PlcLifecycleManager.cs:23, 229—PlcLifecycleManager(Plc) depends back onRecipeSessionand drives itsLoadAsCurrent*methods.Recipes/RecipeDi.cs:21—AddRecipe()registersPlcLifecycleManager(a Plc class), cementing the cycle at registration level; enabling "Recipe" silently pulls the whole Plc graph.Related defects on the same boundary
PlcLifecycleManager.cs:208-225:RegisterReconnectApplyCallback(Func<Recipe, Task<Result>>)is a mutable register-exactly-once setter that the UI must call (RecipeCoordinator.cs:121); its doc says the callback "owns the UI-thread marshalling" — Core control flow designed around a UI concern. Violates the project's constructor-DI-only rule and creates a hidden call-order constraint withInitialize().PlcLifecycleManager.cs:67-77: a separateInitialize()must be called before the object works; forgetting it silently disables connection-state handling.IPlcSyncServiceviolates ISP and sits on the producer side.Plc/IPlcSyncService.cs:8-29:RecipeSessionuses onlyIsSyncEnabled+NotifyRecipeChanged; the lifecycle/control surface (SetSyncEnabled,ResetForDisable,HandleConnectionLost,UpdateConnectionState, observables) is used only byPlcLifecycleManager. Project convention says interfaces belong on the consumer side.Suggested fix
Small, mechanical inversions; dependency then flows one way (Plc → Recipes):
RecipeSession: replace theIPlcSyncServicedependency with an event/IObservable<(Recipe, bool IsValid)>RecipeChanged; the Plc side subscribes.PlcLifecycleManager: replaceRegisterReconnectApplyCallbackwith an event/IObservable<Recipe>ReconnectRecipeAvailable— symmetric with the existingPlcRecipeConflictDetected. FoldInitialize()into construction orEnableSync.IPlcSyncService: a narrowIRecipeSyncNotifierdefined next toRecipeSession(or drop it entirely once the event exists); keep the control surface internal to Plc.PlcLifecycleManagerregistration fromRecipeDiintoS7Di(or a dedicatedAddPlc()).