Compare commits

...

10 Commits

Author SHA1 Message Date
6a9792bb57 Refactor WorkerController to manage job state via API
Removed endpoints for starting/stopping/restarting the Worker service. Controller now uses JobStateManager to get and set the state of specific jobs (e.g., FinalizeDocumentJob) through new GET and POST endpoints. Focus shifts from service lifecycle control to job state management.
2026-04-13 16:31:54 +02:00
6954a86358 Add job state check and improve Worker options handling
Refactored Worker to inject JobStateManager and use the full
WorkerOptions object. Now, FinalizeDocumentJob only runs if
its state is State.Running. Delay is also read from options,
improving configuration and control over job execution.
2026-04-13 16:31:00 +02:00
d6c5b63c49 Register JobStateManager as singleton in DI container
JobStateManager is now added as a singleton service, initialized with the InitialJobState from WorkerOptions. This enables consistent state management across the application by providing a shared instance via dependency injection.
2026-04-13 16:30:27 +02:00
8ca360d47e Add InitialJobState and GdPictureLicenseKey to WorkerOptions
Added InitialJobState as a Dictionary<string, State> with an empty default, and introduced a non-nullable GdPictureLicenseKey property to the WorkerOptions class. These additions support initial job state configuration and GdPicture licensing.
2026-04-13 16:29:14 +02:00
2dadefecc5 Add JobStateManager for thread-safe job state tracking
Introduced JobStateManager class to manage job states using a ConcurrentDictionary, supporting thread-safe get/set operations per job type. Added State enum with Running and Stopped values. Allows optional initial state dictionary for job states.
2026-04-13 16:28:40 +02:00
162f066b08 Refactor Worker to use IOptions for configuration
Replaced direct IConfiguration usage in Worker with IOptions<WorkerOptions> for strongly-typed configuration access. Updated delay interval assignment and added necessary namespace import.
2026-04-13 15:25:17 +02:00
6592642945 Update WorkerOptions config section to "Worker"
Changed AddFinalizeDocumentJob to bind WorkerOptions from the "Worker" section in configuration instead of "WorkerOptions" for correct settings mapping.
2026-04-13 15:22:55 +02:00
855f22cf87 Add DelayMilliseconds property to WorkerOptions
Introduced a configurable DelayMilliseconds property to the WorkerOptions class with validation to ensure the value is at least 1 millisecond. This allows for customizable delay settings in worker operations.
2026-04-13 15:22:34 +02:00
726673e277 Add Windows Services hosting support to ServiceHost
Added Microsoft.Extensions.Hosting.WindowsServices package (v8.0.1) to EnvelopeGenerator.ServiceHost.csproj to enable hosting the application as a Windows Service. No other changes were made.
2026-04-13 12:00:01 +02:00
65d615f43e Support inline PDF display or download via query param
Add a 'download' query parameter to DocResultController's GetAsync method. This lets clients choose whether to download the PDF or display it inline by setting the 'download' parameter in the request.
2026-04-13 11:57:46 +02:00
7 changed files with 76 additions and 49 deletions

View File

@@ -9,8 +9,13 @@ namespace EnvelopeGenerator.ServiceHost.Controllers;
public class DocResultController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAsync([FromQuery] ReadSingleEnvelopeDocResultQuery query, CancellationToken cancel = default)
public async Task<IActionResult> GetAsync([FromQuery] ReadSingleEnvelopeDocResultQuery query, [FromQuery] bool download = false, CancellationToken cancel = default)
{
return File(await mediator.Send(query, cancel), "application/pdf", $"envelope_{query.Envelope.Uuid}.pdf");
var bytes = await mediator.Send(query, cancel);
if (download)
return File(bytes, "application/pdf", $"envelope_{query.Envelope.Uuid}.pdf");
return File(bytes, "application/pdf");
}
}

View File

@@ -1,44 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using EnvelopeGenerator.ServiceHost.Jobs;
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.ServiceHost.Controllers;
[Route("api/[controller]")]
[ApiController]
public class WorkerController(IEnumerable<IHostedService> hostedServices, ILogger<WorkerController> logger) : ControllerBase
public class WorkerController(JobStateManager jobStateManager) : ControllerBase
{
private Worker? Worker => hostedServices.OfType<Worker>().FirstOrDefault();
private readonly JobStateManager _jobStateManager = jobStateManager;
[HttpPost("stop")]
public async Task<IActionResult> Stop(CancellationToken cancel)
[HttpGet(nameof(FinalizeDocumentJob))]
public IActionResult GetStateOfFinalizeDocumentJob() => Ok(_jobStateManager.GetState<FinalizeDocumentJob>());
[HttpPost(nameof(FinalizeDocumentJob))]
public IActionResult SetStateOfFinalizeDocumentJob([FromQuery] State state)
{
if (Worker is null)
return NotFound();
logger.LogInformation("Stopping Worker via API request.");
await Worker.StopAsync(cancel);
return Ok();
}
[HttpPost("start")]
public async Task<IActionResult> Start(CancellationToken cancel)
{
if (Worker is null)
return NotFound();
logger.LogInformation("Starting Worker via API request.");
await Worker.StartAsync(cancel);
return Ok();
}
[HttpPost("restart")]
public async Task<IActionResult> Restart(CancellationToken cancel)
{
if (Worker is null)
return NotFound();
logger.LogInformation("Restarting Worker via API request.");
await Worker.StopAsync(cancel);
await Worker.StartAsync(cancel);
_jobStateManager.SetState<FinalizeDocumentJob>(state);
return Ok();
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
@@ -20,7 +20,8 @@
<PackageReference Include="System.Drawing.Common" Version="8.0.16" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="DevExpress.Reporting.Core" Version="24.2.*" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.17" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.17" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -10,7 +10,13 @@ public static class DependencyInjection
[Obsolete("Check obsoleted services")]
public static IServiceCollection AddFinalizeDocumentJob(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<WorkerOptions>(configuration.GetSection(nameof(WorkerOptions)));
services.Configure<WorkerOptions>(configuration.GetSection("Worker"));
services.AddSingleton(provider =>
{
var options = provider.GetRequiredService<IOptions<WorkerOptions>>().Value;
var manager = new JobStateManager(options.InitialJobState);
return manager;
});
services.AddScoped<FinalizeDocumentJob>();
services.AddScoped<ActionService>();
services.AddSingleton<TempFiles>();

View File

@@ -0,0 +1,19 @@
using AngleSharp.Common;
using System.Collections.Concurrent;
namespace EnvelopeGenerator.ServiceHost.Jobs;
public class JobStateManager(Dictionary<string, State>? initialState = null)
{
private readonly ConcurrentDictionary<Type, State> _states = new();
public State GetState<TJob>() => _states.GetOrAdd(typeof(TJob), type => initialState?.GetOrDefault(type.Name, State.Stopped) ?? State.Stopped);
public State SetState<TJob>(State state) => _states[typeof(TJob)] = state;
}
public enum State
{
Running,
Stopped
}

View File

@@ -1,10 +1,23 @@
using EnvelopeGenerator.ServiceHost.Jobs.FinalizeDocument;
using System.Drawing;
namespace EnvelopeGenerator.ServiceHost.Jobs;
public class WorkerOptions
{
private int _delayMilliseconds = 1000;
public int DelayMilliseconds
{
get => _delayMilliseconds;
set
{
if (value < 1)
throw new ArgumentOutOfRangeException(nameof(value), "Delay must be at least 1 millisecond.");
_delayMilliseconds = value;
}
}
public Dictionary<string, State> InitialJobState { get; set; } = [];
public string GdPictureLicenseKey { get; set; } = null!;
public PDFBurnerOptions PdfBurner { get; set; } = new();

View File

@@ -1,17 +1,20 @@
using EnvelopeGenerator.ServiceHost.Jobs;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.ServiceHost;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly int _delayMilliseconds;
private readonly WorkerOptions _options;
private readonly JobStateManager _jobStateManager;
private readonly IServiceScopeFactory _scopeFactory;
public Worker(ILogger<Worker> logger, IConfiguration configuration, IServiceScopeFactory scopeFactory)
public Worker(ILogger<Worker> logger, IOptions<WorkerOptions> options, JobStateManager jobStateManager, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_delayMilliseconds = Math.Max(1, configuration.GetValue("Worker:DelayMilliseconds", 1000));
_options = options.Value;
_jobStateManager = jobStateManager;
_scopeFactory = scopeFactory;
}
@@ -24,11 +27,14 @@ public class Worker : BackgroundService
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
using var scope = _scopeFactory.CreateScope();
var finalizeDocumentJob = scope.ServiceProvider.GetRequiredService<FinalizeDocumentJob>();
await finalizeDocumentJob.ExecuteAsync(stoppingToken);
if (_jobStateManager.GetState<FinalizeDocumentJob>() == State.Running)
{
using var scope = _scopeFactory.CreateScope();
var finalizeDocumentJob = scope.ServiceProvider.GetRequiredService<FinalizeDocumentJob>();
await finalizeDocumentJob.ExecuteAsync(stoppingToken);
}
await Task.Delay(_delayMilliseconds, stoppingToken);
await Task.Delay(_options.DelayMilliseconds, stoppingToken);
}
}
}
}