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.
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 viaNew TempFiles(LogConfig). This is called on every job execution and manages the job's own temp lifecycle. It expectsLogConfig. 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 usingILogger<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 legacyMy.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(forAddWindowsService())
- 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
}
}
}
}
FontStyleinteger: Regular=0, Bold=1, Italic=2, Underline=4, Strikeout=8. All values mirrorPDFBurnerParams.vbdefaults 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 filesCleanUp()? delete the directory on service stop
Does NOT replace
CommonServices.TempFiles— that one continues to be used inside each job viaLogConfig.
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):
? replaced byProjectInstallersc create/New-Service? BackgroundService handles thisService.Designer.vb? not applicableService.resx
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.