Files
EnvelopeGenerator/EnvelopeGenerator.Service/MIGRATION_PLAN.md
TekH eb219291de Migrate WebUI to EnvelopeGenerator.Server
Updated documentation to reflect the migration from WebUI to EnvelopeGenerator.Server, including a new "Documentation Catalog" in `AGENTS.md`. Adjusted route mappings, render modes, and directory structure for Blazor components.

Revised coordinate system documentation and CSS for status colors to align with the new architecture. Updated YARP proxy and PDF.js configuration paths.

Migrated `MIGRATION_PLAN.md` to modernize `EnvelopeGenerator.Service` with C# Worker Service equivalents. Updated `FORM_APPLICATION_CONTEXT.md` and `RECEIVER_PDF_VIEWER_CONTEXT.md` to reflect the new architecture and planned viewer integration.

Fixed German translation issues in `fix-report-label-read-and-confirmed.md`. Standardized formatting and terminology across all files.
2026-09-22 16:17:09 +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.