using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.OtOpcUa.Configuration.LocalCache; namespace ZB.MOM.WW.OtOpcUa.Server; /// /// DI registration helpers consumed by Program.cs. Extracted so tests can assert /// the production wire-up actually composes without spinning up the full Host. /// public static class ServerWiring { /// /// Server-014 — registers the Phase 6.1 Stream D generation-sealed bootstrap chain: /// , , /// , and . Without these /// registrations OpcUaServerService cannot consume the sealed bootstrap and the /// stays inert — /healthz's usingStaleConfig /// never flips on a DB outage with a warm cache. /// /// /// The cache root is sourced from — same path /// the legacy uses for its LiteDB cache, so both bootstrap /// paths persist alongside each other while the migration completes. /// public static IServiceCollection AddSealedBootstrap(this IServiceCollection services, NodeOptions options) { // Use a sibling directory off LocalCachePath so the LiteDB file and the // GenerationSealedCache snapshots don't clash. The cache root is a directory; // LocalCachePath is canonically the LiteDB file path. var cacheRoot = ResolveCacheRoot(options.LocalCachePath); // Register NodeOptions only if the caller hasn't already done so — Program.cs // registers it earlier in its DI chain, but the wiring helper supports standalone // unit tests that want to compose just the SealedBootstrap chain. if (!services.Any(d => d.ServiceType == typeof(NodeOptions))) services.AddSingleton(options); services.AddSingleton(new GenerationSealedCache(cacheRoot)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); return services; } private static string ResolveCacheRoot(string localCachePath) { // LocalCachePath is the LiteDB file (e.g. "config_cache.db"); the sealed cache is a // directory. Pick a sibling folder so the two don't share a path. if (string.IsNullOrWhiteSpace(localCachePath)) return Path.Combine(Path.GetTempPath(), "otopcua-sealed-cache"); var dir = Path.GetDirectoryName(localCachePath); var name = Path.GetFileNameWithoutExtension(localCachePath); var root = string.IsNullOrEmpty(dir) ? $"{name}.sealed" : Path.Combine(dir, $"{name}.sealed"); return root; } }