# Migration Plan: EnvelopeGenerator.Service_legacy ? EnvelopeGenerator.Service **Revision v3** — Full deep analysis complete ## Overview Migrate the legacy VB.NET Windows Service (`EnvelopeGenerator.Service_legacy`) to a modern C# Worker Service (`EnvelopeGenerator.Service`) targeting **.NET Framework 4.6.2**, using `Microsoft.Extensions.Hosting`, `IConfiguration`, `ILogger` and **NLog**. --- ## Deep Analysis Findings (vs. v1/v2) ### Finding 1 — Connection String: Plain Text, No Encryption Required `FinalizeDocumentJob` and `APIEnvelopeJob` call `MSSQLServer.DecryptConnectionString()` on the `JobDataMap[Value.DATABASE]` value. However, `MSSQLServer.DecryptConnectionString()` is a pass-through for plain-text strings — no wrapping/encryption step is needed. **Resolution**: The connection string is read from `IConfiguration` via the standard `ConnectionStrings:Default` key (same key as `appsettings.Database.json` in `EnvelopeGenerator.Web`). It is passed **as-is** to both the Worker-level `MSSQLServer` and the `JobDataMap`. No `EncryptConnectionString()` call is made anywhere in the new service. ### Finding 2 — Dual TempFiles Architecture There are **two independent** `TempFiles` classes: - `EnvelopeGenerator.CommonServices.TempFiles` — used **inside each job** via `New TempFiles(LogConfig)`. This is called on every job execution and manages the job's own temp lifecycle. It expects `LogConfig`. **We do not touch this.** - `EnvelopeGenerator.Service_legacy.TempFiles` — used at the **service/host level** (startup cleanup, shutdown cleanup). This is the one we rewrite in C# in the Service project using `ILogger`. ### Finding 3 — LogConfig is a Hard Dependency of All Jobs `FinalizeDocumentJob.Execute()` and `APIEnvelopeJob.Execute()` both do: ```vb LogConfig = pContext.MergedJobDataMap.Item(Value.LOGCONFIG) Logger = LogConfig.GetLogger() myTempFiles = New TempFiles(LogConfig) ' ? CommonServices.TempFiles ``` `LogConfig` is **not optional**. The schedulers must construct a valid `LogConfig` and place it in the `JobDataMap`. It must carry the correct log path, debug flag, and application identity. **Resolution**: A `LogConfigFactory` static helper class will be created in the Service project to build a `LogConfig` from `ServiceConfig` values: - `LogPath` = `Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log")` (mirrors legacy `My.Application.Info.DirectoryPath + "\Log"`) - `Debug` = `ServiceConfig.Debug` - Application name = `"EnvelopeGenerator.Service"` ### Finding 4 — ProjectInstaller is Obsolete `ProjectInstaller.vb` was the old `installutil.exe` mechanism for Windows Service registration. With `AddWindowsService()` + `sc create` / PowerShell `New-Service`, this is **completely replaced**. No equivalent is needed in the new project. ### Finding 5 — GDPicture License SQL The legacy service queries: `SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME = 'GDPICTURE' and ACTIVE = 1` This must be replicated exactly in the Worker startup sequence. If the result is null/empty, startup fails with a descriptive exception (matching legacy behavior). ### Finding 6 — Quartz Version Mismatch `CommonServices` uses `Quartz 3.8.0`. `Service_legacy` uses `Quartz 3.15.0`. The new `EnvelopeGenerator.Service` must use **Quartz 3.8.0** to match `CommonServices` (same AppDomain, same version must be loaded). The legacy version bump in `Service_legacy` was inconsistent. --- ## 1. Project File (`EnvelopeGenerator.Service.csproj`) - Add **NuGet packages**: - `NLog` (5.x, matching CommonServices' `NLog.5.0.5`) - `NLog.Extensions.Logging` (latest net462-compatible) - `Quartz` (**3.8.0** — must match CommonServices) - `Microsoft.Extensions.Hosting.WindowsServices` (for `AddWindowsService()`) - Add **project reference** to `EnvelopeGenerator.CommonServices` (vbproj) - Add **project reference** to `EnvelopeGenerator.Domain` (csproj) --- ## 2. Configuration (`ServiceConfig.cs`) ```csharp public class ServiceConfig { public string ConnectionString { get; set; } = string.Empty; public bool Debug { get; set; } = false; public int IntervalInMin { get; set; } = 1; public PDFBurnerParams PDFBurnerParams { get; set; } = new PDFBurnerParams(); } ``` Connection string is read via the standard `ConnectionStrings:Default` key — consistent with `appsettings.Database.json` used by `EnvelopeGenerator.Web`: `appsettings.json`: ```json { "ConnectionStrings": { "Default": "" }, "ServiceConfig": { "Debug": false, "IntervalInMin": 1, "PDFBurnerParams": { "IgnoredLabels": [ "Date", "Datum", "ZIP", "PLZ", "Place", "Ort", "Position", "Stellung" ], "TopMargin": 0.1, "YOffset": -0.3, "FontName": "Arial", "FontSize": 8, "FontStyle": 2, "TruncationSuffix": "..", "TextMaxWidths": { "position": 1.41, "city": 1.41, "date": 2.0 } } } } ``` > `FontStyle` integer: Regular=0, Bold=1, **Italic=2**, Underline=4, Strikeout=8. > All values mirror `PDFBurnerParams.vb` defaults exactly. --- ## 3. NLog Setup **`nlog.config`**: File target ? `${basedir}/Log/service-${shortdate}.log`, async wrapper, layout with timestamp + level + logger + message + exception. **`Program.cs`**: Wire via `NLog.Extensions.Logging`: ```csharp builder.Logging.ClearProviders(); builder.Logging.AddNLog("nlog.config"); ``` --- ## 4. LogConfigFactory (`LogConfigFactory.cs`) New static helper — bridges `ServiceConfig` ? `LogConfig` (required by CommonServices jobs): ```csharp internal static class LogConfigFactory { public static LogConfig Create(ServiceConfig config) { var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log"); var logConfig = new LogConfig( LogConfig.PathType.CustomPath, logPath, null, "Digital Data", "EnvelopeGenerator.Service"); logConfig.Debug = config.Debug; return logConfig; } } ``` This `LogConfig` instance is what gets placed into every `JobDataMap[Value.LOGCONFIG]`. --- ## 5. Quartz LogProvider (`QuartzLogProvider.cs`) C# rewrite of `LogProvider.vb` — bridges Quartz `ILogProvider` ? `ILogger`: ```csharp internal class QuartzLogProvider : ILogProvider { private readonly ILogger _logger; public QuartzLogProvider(ILogger logger) => _logger = logger; public Logger GetLogger(string name) => (level, func, ex, parameters) => { ... }; // OpenNestedContext / OpenMappedContext ? NotImplementedException (same as legacy) } ``` --- ## 6. TempFiles (`TempFiles.cs`) — Service-level only Service-level rewrite of `Service_legacy/TempFiles.vb` using `ILogger`: - `Create()` ? create `%TEMP%\EnvelopeGenerator`, or clean existing files - `CleanUp()` ? delete the directory on service stop > **Does NOT replace** `CommonServices.TempFiles` — that one continues to be used > inside each job via `LogConfig`. --- ## 7. Schedulers ### `Schedulers/SchedulerFinishEnvelope.cs` ```csharp public class SchedulerFinishEnvelope { // Constructor: ILogger, IOptions // Start(licenseKey, logConfig): // - Build JobDataMap: // [GDPICTURE] = licenseKey // [LOGCONFIG] = logConfig ? LogConfigFactory output // [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString) // [PDF_BURNER_PARAMS] = config.PDFBurnerParams // - Schedule FinalizeDocumentJob with Quartz (interval from config) // - Register QuartzLogProvider // Stop(): Scheduler.Shutdown() } ``` ### `Schedulers/SchedulerEnvelopeTaskApi.cs` ```csharp public class SchedulerEnvelopeTaskApi { // Constructor: ILogger, IOptions // Start(logConfig): // - Build JobDataMap: // [LOGCONFIG] = logConfig // [DATABASE] = MSSQLServer.EncryptConnectionString(connectionString) // - Schedule APIEnvelopeJob with Quartz (interval from config) // - Register QuartzLogProvider // Stop(): Scheduler.Shutdown() } ``` --- ## 8. Worker (`Worker.cs`) Full lifecycle replacing the placeholder: ``` ExecuteAsync: 1. Read ServiceConfig from IOptions 2. Validate ConnectionString ? throw if empty 3. Build LogConfig via LogConfigFactory 4. Connect to DB (plain connection string — Worker-level only) 5. Query: SELECT LICENSE FROM TBDD_3RD_PARTY_MODULES WHERE NAME='GDPICTURE' AND ACTIVE=1 ? throw if null/empty (matches legacy behavior) 6. TempFiles.Create() 7. await SchedulerFinishEnvelope.Start(licenseKey, logConfig) 8. await Task.Delay(2500, stoppingToken) ? preserved from legacy 9. await SchedulerEnvelopeTaskApi.Start(logConfig) 10. await Task.Delay(Timeout.Infinite, stoppingToken) ? wait for cancellation StopAsync override: 11. await SchedulerFinishEnvelope.Stop() 12. await SchedulerEnvelopeTaskApi.Stop() 13. TempFiles.CleanUp() ``` --- ## 9. Program.cs ```csharp var builder = Host.CreateApplicationBuilder(args); builder.Logging.ClearProviders(); builder.Logging.AddNLog("nlog.config"); builder.Services.Configure( builder.Configuration.GetSection("ServiceConfig")); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddWindowsService(o => o.ServiceName = "EnvelopeGenerator.Service"); var host = builder.Build(); host.Run(); ``` --- ## 10. Files to Create / Modify | File | Action | Notes | |------|--------|-------| | `EnvelopeGenerator.Service.csproj` | **Modify** | Add packages, project refs | | `Program.cs` | **Modify** | Full DI/NLog/WindowsService setup | | `Worker.cs` | **Modify** | Full service lifecycle | | `ServiceConfig.cs` | **Create** | | | `LogConfigFactory.cs` | **Create** | Critical bridge: ServiceConfig ? LogConfig | | `TempFiles.cs` | **Create** | Service-level only; CommonServices.TempFiles untouched | | `QuartzLogProvider.cs` | **Create** | | | `Schedulers/SchedulerFinishEnvelope.cs` | **Create** | | | `Schedulers/SchedulerEnvelopeTaskApi.cs` | **Create** | | | `appsettings.json` | **Create** | All PDFBurnerParams explicit | | `nlog.config` | **Create** | | **Not created** (obsolete in new architecture): - ~~`ProjectInstaller`~~ ? replaced by `sc create` / `New-Service` - ~~`Service.Designer.vb`~~ ? BackgroundService handles this - ~~`Service.resx`~~ ? not applicable --- ## 11. Key Design Decisions | Decision | Rationale | |----------|-----------| | **Quartz 3.8.0** (not 3.15.0) | Must match CommonServices; same AppDomain, one version | | **Connection string plain-text, no encryption** | `MSSQLServer.DecryptConnectionString()` is a pass-through for plain strings; `ConnectionStrings:Default` key used (same convention as Web project) | | **Connection string in `ConnectionStrings:Default`** | Consistent with rest of the solution (`appsettings.Database.json`); `ServiceConfig` holds only service-specific settings | | **LogConfigFactory** | Jobs have hard dependency on `LogConfig` in JobDataMap; this is the clean bridge without leaking LogConfig into the DI container | | **2500ms delay preserved** | Legacy behavior; ensures Scheduler1 is fully initialized before Scheduler2 starts | | **`AddWindowsService()`** | Native SCM integration; replaces ProjectInstaller/installutil entirely | | **NLog log path = BaseDirectory/Log** | Mirrors `My.Application.Info.DirectoryPath + "\Log"` from legacy | | **CommonServices.TempFiles untouched** | Jobs create their own TempFiles internally via LogConfig; service-level TempFiles is separate concern | --- ## Approval Required > Please review this plan and confirm before implementation begins.