Files
EnvelopeGenerator/EnvelopeGenerator.Service/MIGRATION_PLAN.md
TekH 665266457f Migrate legacy VB.NET service to modern C# Worker
Migrated the legacy VB.NET Windows Service to a modern C# Worker Service targeting .NET Framework 4.6.2. Key changes include:

- Added `Microsoft.Extensions.Hosting.WindowsServices`, `NLog`, and `Quartz` for logging and scheduling.
- Introduced `LogConfigFactory` to bridge `ServiceConfig` and `LogConfig` for Quartz jobs.
- Rewrote `Worker` to handle the full service lifecycle, including database connection, temp file management, and scheduler initialization.
- Implemented `SchedulerFinishEnvelope` and `SchedulerEnvelopeTaskApi` for Quartz job scheduling.
- Created `TempFiles` for service-level temp file management.
- Configured `NLog` for file and console logging.
- Updated `appsettings.json` for connection strings and service-specific configuration.
- Replaced legacy `ProjectInstaller` with `AddWindowsService()` for SCM integration.
2026-08-27 11:39:49 +02:00

12 KiB

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<T> 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<TempFiles>.

Finding 3 — LogConfig is a Hard Dependency of All Jobs

FinalizeDocumentJob.Execute() and APIEnvelopeJob.Execute() both do:

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)

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:

{
  "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:

builder.Logging.ClearProviders();
builder.Logging.AddNLog("nlog.config");

4. LogConfigFactory (LogConfigFactory.cs)

New static helper — bridges ServiceConfig ? LogConfig (required by CommonServices jobs):

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<T>:

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<TempFiles>:

  • 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

public class SchedulerFinishEnvelope
{
    // Constructor: ILogger<SchedulerFinishEnvelope>, IOptions<ServiceConfig>
    // 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

public class SchedulerEnvelopeTaskApi
{
    // Constructor: ILogger<SchedulerEnvelopeTaskApi>, IOptions<ServiceConfig>
    // 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<ServiceConfig>
  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

var builder = Host.CreateApplicationBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddNLog("nlog.config");

builder.Services.Configure<ServiceConfig>(
    builder.Configuration.GetSection("ServiceConfig"));

builder.Services.AddSingleton<TempFiles>();
builder.Services.AddSingleton<SchedulerFinishEnvelope>();
builder.Services.AddSingleton<SchedulerEnvelopeTaskApi>();
builder.Services.AddHostedService<Worker>();
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.