using EnablementBLL.ShowResolver; using EnablementBLL.ShowRule; using EnablementDAL.CustomCode.ShowRequestLog; using EnablementDAL.CustomCode.ShowResolver; using EnablementDAL.Implementations; using CMSDAL.CustomCode.Channel; using FastEndpoints; using FastEndpoints.Swagger; using GB5Shared.Connection; using GB5Shared.DaprCache; using GB5Shared.Telemetry; using GB5Shared.DateConverter; using static GB5Shared.DateConverter.GB5JsonOptions; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.EventLogPublish; using GB5Shared.GB5CommonFunction; using GB5Shared.GenerateAutoNumber; using GB5Shared.PickListGenerator; using GB5Shared.QueryExecutor; using GB5Shared.Signatory; using GB5Shared.Validation; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Hosting; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using System.Reflection; using System.Text; // ============================================================ // Bootstrap // ============================================================ var builder = WebApplication.CreateBuilder(args); // ============================================================ // Console Encoding // ============================================================ Console.OutputEncoding = Encoding.UTF8; // ============================================================ // Background Service Exception Handling // ============================================================ builder.Services.Configure(options => { options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore; }); // ============================================================ // Configure Web Host & Port // ============================================================ var appPort = builder.Configuration.GetValue("AppPort"); builder.WebHost.UseUrls($"http://0.0.0.0:{appPort}"); // ============================================================ // Dapr & HttpClient // ============================================================ builder.Services.AddDaprClient(); builder.Services.AddHttpClient(); // ============================================================ // Controllers // ============================================================ builder.Services .AddControllers() .AddDapr() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.DictionaryKeyPolicy = null; options.JsonSerializerOptions.AddGB5Converters(); }); // ============================================================ // API Behavior // ============================================================ builder.Services.Configure(options => { options.SuppressModelStateInvalidFilter = true; }); // ============================================================ // JSON Global Options // ============================================================ builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNamingPolicy = null; options.SerializerOptions.DictionaryKeyPolicy = null; options.SerializerOptions.AddGB5Converters(); }); // ============================================================ // Caching // ============================================================ #pragma warning disable EXTEXP0018 builder.Services.AddHybridCache(options => { options.DefaultEntryOptions = new HybridCacheEntryOptions(); options.DisableCompression = false; }); #pragma warning restore EXTEXP0018 builder.Services.AddMemoryCache(); builder.Services.AddDistributedMemoryCache(); // ============================================================ // Common GB5 Services // ============================================================ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Required by TMSBLL.TrainingProgrammeSessionTopic.TrainingProgrammeSessionTopicBLL // (Dapr distributed-cache key invalidation on Topic writes). GB5Shared is never // assembly-scanned in this service (see convention above), so this must be // registered manually like AutoNumber / EventLogPublish. builder.Services.AddScoped(); // Required by GB5Shared.Signatory.SignatoryBLL (ISignatoryBLL's own DAL // dependency). GB5Shared is never assembly-scanned in this service, so // this must be registered manually alongside ISignatoryBLL below. builder.Services.AddScoped(); builder.Services.AddScoped(); // ============================================================ // Enablement DAL // ============================================================ builder.Services.AddScoped(); builder.Services.AddScoped(); // ============================================================ // CMS DAL // Required by FrameworkBLL.Channel.ChannelBLL // // ChannelBLL // ↓ // IChannelDAL // ↓ // ChannelDAL // ============================================================ builder.Services.AddScoped(); // ============================================================ // Enablement BLL // ============================================================ builder.Services.AddScoped(); builder.Services.AddScoped(); // ============================================================ // Configuration Binding // ============================================================ builder.Services.Configure( builder.Configuration.GetSection("Gb5SystemDTO")); // ============================================================ // Kestrel // ============================================================ builder.Services.Configure(options => { options.AllowSynchronousIO = true; }); // ============================================================ // OpenTelemetry // ============================================================ builder.Services.AddGB5Telemetry( builder.Configuration, "GB5-E"); // ============================================================ // Assembly Scan // ============================================================ var EnablementBLLAssembly = Assembly.Load("EnablementBLL"); var EnablementDALAssembly = Assembly.Load("EnablementDAL"); // IMPORTANT: // CMSDAL contains IChannelDAL / ChannelDAL. // Therefore CMSDAL must also be scanned if you want // automatic registration of its other interfaces. var CMSDALAssembly = Assembly.Load("CMSDAL"); // IMPORTANT: // CMSBLL contains IChannelBLL / ChannelBLL, which is required by // EnablementBLL.ShowContentLink.ShowContentLinkBLL (cross-module // dependency introduced for the CMS auto-link feature). Without // loading + scanning this assembly, DI cannot resolve IChannelBLL // and app startup fails with: // "Unable to resolve service for type 'CMSBLL.Channel.IChannelBLL' // while attempting to activate 'EnablementBLL.ShowContentLink.ShowContentLinkBLL'." var CMSBLLAssembly = Assembly.Load("CMSBLL"); // IMPORTANT: // ShowContentLinkBLL also depends on WiBLL.WiMaster.IWiMasterBLL // (Work Instruction title refresh) and // TMSBLL.TrainingProgrammeSessionTopic.ITrainingProgrammeSessionTopicBLL // (Training Topic title refresh). Both are cross-module dependencies // from the same feature and neither assembly was previously loaded // or scanned, so DI cannot resolve them without adding these too. var WiBLLAssembly = Assembly.Load("WiBLL"); var TMSBLLAssembly = Assembly.Load("TMSBLL"); // IMPORTANT: // WiMasterBLL and TrainingProgrammeSessionTopicBLL in turn depend on // their own DAL interfaces (e.g. IWiMasterDAL in WiDAL, // ITrainingProgrammeSessionTopicDAL in TMSDAL — see the WiDAL.DTOs / // TMSDAL.DTO.TrainingProgramme usings in ShowContentLinkBLL.cs). // Adding these proactively to avoid resolving this chain one // interface at a time. var WiDALAssembly = Assembly.Load("WiDAL"); var TMSDALAssembly = Assembly.Load("TMSDAL"); // IMPORTANT: // Scanning TMSBLL wholesale also pulls in TmsFeedbackBLL and // AssessmentFlsCoordinatorBLL, which depend on FLSBLL.Instance.IFlsInstanceBLL // and FLSBLL.Registration.IFlsRegistrationBLL respectively. FLSBLL was never // loaded/scanned, so ValidateOnBuild fails on these unrelated TMS classes too. var FLSBLLAssembly = Assembly.Load("FLSBLL"); // FLSBLL's classes (pulled in via TMSBLL's Feedback/AssessmentFlsBridge // dependencies) in turn depend on FLSDAL.CustomCode.* interfaces // (IFlsInstanceDAL, ISurveyAnswerDAL, IFlsSessionDAL, IFlsRespondentDAL, // IFlsRegistrationDAL, IQuickRatingDAL, IFlsStepSequenceDAL, IFlsEventDAL, // IFlsBridgeDAL, etc). Adding FLSDAL resolves all of these via convention. var FLSDALAssembly = Assembly.Load("FLSDAL"); builder.Services.Scan(scan => scan .FromAssemblies( EnablementBLLAssembly, EnablementDALAssembly, CMSDALAssembly, CMSBLLAssembly, WiBLLAssembly, TMSBLLAssembly, WiDALAssembly, TMSDALAssembly, FLSBLLAssembly, FLSDALAssembly) .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface && (t.Namespace == null || !t.Namespace.Contains(".DTO")))) .AsImplementedInterfaces() .WithScopedLifetime()); // ============================================================ // FastEndpoints // ============================================================ builder.Services.AddFastEndpoints(); // ============================================================ // Swagger // ============================================================ builder.Services.SwaggerDocument(o => { o.DocumentSettings = s => { s.Title = "GB5 Enablement API"; s.Version = "v1"; s.Description = "GB5 Enablement Microservice"; }; }); // ============================================================ // Service Provider Validation // ============================================================ // // TMSBLL / FLSBLL pull in several loosely-related feature areas // (Certificate PDF generation, Digital Signatures, FLS surveys) that // are not used by EnablementSL's actual endpoints (ShowContentLink, // ShowResolver, ShowRule), but ValidateOnBuild eagerly constructs // EVERY registered service at startup - including ones this service // never calls - so an unrelated missing dependency anywhere in those // assemblies blocks the whole app from starting. // // Disabling ValidateOnBuild here means DI resolution errors for // unused services (e.g. IDigitalSignatureService, ISignatoryDAL if // still incomplete) will only surface if/when something actually // requests them at runtime, not at every app startup. This does NOT // disable ValidateScopes (a scoped service being captured by a // singleton would still be caught). // // TODO: once TMSBLL/FLSBLL registrations are fully resolved (or that // module's DI setup is separated out), consider re-enabling this. builder.Host.UseDefaultServiceProvider(options => { options.ValidateOnBuild = false; options.ValidateScopes = true; }); // ============================================================ // Build Application // ============================================================ var app = builder.Build(); // ============================================================ // Middleware Pipeline // ============================================================ // ------------------------------------------------------------ // 1. Forwarded Headers // ------------------------------------------------------------ app.UseForwardedHeaders(); // ------------------------------------------------------------ // 2. Telemetry & Session Middleware // ------------------------------------------------------------ app.UseMiddleware< GB5Shared.Telemetry.Middleware.RequestTracingMiddleware>(); app.UseMiddleware< GB5Shared.Middleware.SessionHeartbeatMiddleware>(); // ------------------------------------------------------------ // 3. FastEndpoints // ------------------------------------------------------------ app.UseFastEndpoints(c => { c.Serializer.Options.PropertyNamingPolicy = null; c.Serializer.Options.DictionaryKeyPolicy = null; c.Serializer.Options.AddGB5Converters(); }); // ------------------------------------------------------------ // 4. Swagger // ------------------------------------------------------------ app.UseOpenApi(); app.UseSwaggerUi(o => { o.Path = "/GB5Documentation"; o.DocumentPath = "/swagger/{documentName}/swagger.json"; o.TransformToExternalPath = (internalUiRoute, _) => "/ena" + internalUiRoute; }); // ------------------------------------------------------------ // 5. Dapr CloudEvents // ------------------------------------------------------------ app.UseCloudEvents(); app.MapSubscribeHandler(); // ------------------------------------------------------------ // 6. MVC Controllers // ------------------------------------------------------------ app.MapControllers(); // ------------------------------------------------------------ // 7. Authorization // ------------------------------------------------------------ app.UseAuthorization(); // ============================================================ // Health / Liveness // ============================================================ app.MapGet( "/", () => "Hello from GB5 Enablement Service (.NET 9)"); // ============================================================ // Run // ============================================================ app.Run();