using System.Reflection; using GB5Shared.ExcelExport; using GB5Shared.Hosting; using GB5Shared.Storage; using IceImportBLL.IceImportRun; using IceImportBLL.IceImportRunNotifier; using IceImportSL.Hubs; using GB5Shared.Vault; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using VaultSharp; using VaultSharp.V1.AuthMethods.Token; namespace IceImportSL; // Modeled on GB5Solution/Automation/AutomationSL/AutomationModule.cs — the same // "Program.cs stays vestigial, real bootstrap lives in a Module : IModule" pattern PlatformHost's // Program.cs already uses for every other module (see the hardcoded `new XxxModule()` list there). // // Design decision: IceImport's UploadImportFile -> CommitImport handoff does NOT go through // GB5Shared.Attachment.IAttachmentUploadService/TATTACHMENT, even though the Phase 1 task // description named it as a reference. That service's UploadAsync requires a real // MDOCUMENTSETDETAIL row (DocumentSetDetailId) plus an ObjectTypeId/BizTransactionTypeId shape // designed for formal document management (folders, templates, SP_ATTACHMENT_GET_NEXT_SLNO_VERSION, // SP_ATTACHMENT_INSERT) — no IceMap has (or should need) that config for a transient ETL source // file, and fabricating a placeholder DocumentSetDetailId risks tripping FK/trigger constraints on // TATTACHMENT that could not be verified against a live schema in this session (gb5-schema MCP // unavailable). Instead, IIceImportUploadStagingService writes the raw bytes via // GB5Shared.Storage.IStorageProvider directly (still one of the two named reference assets) and // hands back a Microsoft.AspNetCore.DataProtection-signed opaque token — no new DB table, no // fabricated FK values, satisfies IStorageProvider's "never expose the raw storagePath" contract. public class IceImportModule : IModule { public void Register(IServiceCollection services, IConfiguration configuration) { services.AddGB5Storage(configuration.GetSection("StorageConfiguration")); // Needed directly by Phase 4's IIceMapFtpSourceBLL/IceMapFtpSourceDAL and // IIceImportSecretResolver (Vault-backed *VaultRef resolution) — not incidental. services.AddGB5Vault(configuration); // CsvSourceFileParser/ExcelSourceFileParser (IceImportDAL) need // GB5Shared.FileImport.IFileToDataTableService. GB5Shared is a raw Reference (not a // ProjectReference), so Scrutor's assembly scan never sees it — MMSL/AdminModule hit the // identical gap and fixed it the same way. services.AddScoped(); // IceImportPipeline/ScheduledImportService/IceImportRunExecutionService/IceMapFtpSourceBLL // depend on GB5Shared.IceMap.IIceMapBLL (mapping metadata CRUD), and MenuWebServiceDispatcher // depends on GB5Shared.ActionProcessor.IWebhookEndpointResolver — both now shared-library // types (moved out of FrameworkBLL/FrameworkDAL), registered explicitly here rather than via // assembly scan, matching the original narrow-registration reasoning below. services.AddScoped(); services.AddScoped(); services.AddScoped(); // IDataProtectionProvider backing IIceImportUploadStagingService's opaque upload token. services.AddDataProtection(); // Used by IMenuWebServiceDispatcher/MenuWebServiceDispatcher (persistence + lookup // fallback) via IWebhookEndpointResolver — same named client FrameworkSL registers for the // same handler; IceImportSL is a different host, so it needs its own registration. // Timeout raised well past HttpClient's 100s default — InvokeSaveBatchAsync (REQUESTSCHEMATYPE // = 0) can post a list of 1000+ rows in one call, and the target endpoint's own processing // time for that payload routinely exceeds 100s. Dispatch always runs inside // IceImportRunExecutionService's background-tracked run, never on a request thread a user is // synchronously blocked on, so a longer timeout costs nothing but how quickly a genuinely // hung call is detected as failed. services.AddHttpClient("webhook", client => { client.Timeout = TimeSpan.FromMinutes(30); }); // IceImportRunHub — live progress for RunDetail/import-monitor UI (Phase 3). Options mirror // AutomationModule's AddSignalR call and CLAUDE.md's SignalR registration template. services.AddSignalR(options => { options.EnableDetailedErrors = true; options.MaximumReceiveMessageSize = 32 * 1024; options.ClientTimeoutInterval = TimeSpan.FromSeconds(60); options.KeepAliveInterval = TimeSpan.FromSeconds(15); }); // IceImportRunExecutionService's error-report generation (Phase 2) needs IExcelExport — // registered explicitly per-module, same as every other module that consumes it // (AccountsModule/MMModule/AnalyticsModule all do the identical // services.AddScoped() — GB5Shared.dll is a raw Reference, not a // ProjectReference, so Scrutor's assembly scan below never sees it). services.AddScoped(); // Phase 4 — IIceImportSecretResolver's VaultSharp/IMemoryCache dependencies. Registration // mirrors GB5Solution/PAY/PAYSL/PAYModule.cs's identical "Vault — Singleton (thread-safe // IVaultClient + in-memory secret cache)" block exactly (the only existing precedent for // this in the repo; see IIceImportSecretResolver's doc comment). services.AddMemoryCache(); services.AddSingleton(sp => { var authMethod = new TokenAuthMethodInfo(configuration["Vault:Token"]!); var settings = new VaultClientSettings(configuration["Vault:Address"]!, authMethod); return new VaultClient(settings); }); var bll = Assembly.Load("IceImportBLL"); var dal = Assembly.Load("IceImportDAL"); // Scoped to IceImportBLL/IceImportDAL only — see the IIceMapBLL/IWebhookEndpointResolver // registrations above for why FrameworkBLL/FrameworkDAL are deliberately NOT scanned here. services.Scan(scan => scan .FromAssemblies(bll, dal) // Canonical filter (mirrors GB5Framework/FrameworkSL/Program.cs's scan): // - IHostedService: hosted services are registered separately with their own lifetime. // - Exception: Exception implements ISerializable/_Exception, so AsImplementedInterfaces() // would otherwise register exception types (e.g. QualifierActivationException) as // services, and DI then fails trying to activate them via their (string) constructor. // - records (detected via the compiler-generated $ method): a record implicitly // implements IEquatable, so without this, DTOs/records get registered as services // and the container fails trying to activate them via their data constructor. .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface && !typeof(IHostedService).IsAssignableFrom(t) && !typeof(Exception).IsAssignableFrom(t) && t.GetMethod("$") == null && !t.Name.EndsWith("ListDTO") && !t.Name.EndsWith("DTO"))) .AsImplementedInterfaces() .WithScopedLifetime()); // Both registered AFTER the Scan call above so they win over the Scoped registrations // Scrutor would otherwise add for these same concrete types (last registration wins when // Microsoft.Extensions.DependencyInjection resolves a single instance) — IceImportRunHub // push needs a real Singleton IHubContext-backed notifier (mirrors // AutomationSL.Hubs.AutomationRunNotifier's identical Singleton registration), and the // cancellation registry must be one shared Singleton dictionary per instance, not // one-per-request, or CancelImportRun could never find the CTS a background Task created // in a completely different scope. services.AddSingleton(); services.AddSingleton(); } public void MapEndpoints(WebApplication app) { app.MapHub("/hubs/iceimport-run"); } }