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.
316 lines
12 KiB
Markdown
316 lines
12 KiB
Markdown
# 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:
|
|
```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<T>`:
|
|
|
|
```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<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`
|
|
|
|
```csharp
|
|
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`
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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.
|
|
|