Add XML documentation for improved code clarity

Added detailed XML documentation across multiple files to enhance
code maintainability and readability. Key updates include:

- Documented `AllowAllDashboardAuthorizationFilter` to clarify
  its development-only usage.
- Added comments to `DtoExtensions` for Hangfire job ID generation
  and DTO-to-command conversion methods.
- Enhanced `HealthCheckHtmlGenerator` with detailed descriptions
  of HTML generation methods and utility functions.
- Documented `DependencyInjection` and `ProfileWorkerOptionsValidator`
  to explain service registration and configuration validation.
- Updated `ProfileCache` with comments on thread-safe operations.
- Added documentation to `ProfileWork` for profile synchronization
  logic and execution flow.
- Enhanced `ProfileWorker` with health check logic and background
  service execution details.
- Documented `ProfileWorkerOptions` configuration properties.

These changes aim to improve developer understanding and ensure
best practices are followed in production environments.
This commit is contained in:
2026-07-13 13:37:12 +02:00
parent 84b95c53e4
commit bc881bf70f
8 changed files with 254 additions and 5 deletions

View File

@@ -2,8 +2,26 @@
namespace ECMJobRunner.WebCron.ProfileWorker;
/// <summary>
/// Extension methods for configuring ProfileWorker services in the DI container.
/// </summary>
public static class DependencyInjection
{
/// <summary>
/// Registers ProfileWorker background service and related dependencies.
/// </summary>
/// <param name="services">The service collection to configure.</param>
/// <param name="configuration">Application configuration containing ProfileWorker settings.</param>
/// <returns>The configured service collection for method chaining.</returns>
/// <remarks>
/// Registers the following services:
/// <list type="bullet">
/// <item><description><see cref="ProfileWorkerOptions"/> - Configuration options from appsettings.json</description></item>
/// <item><description><see cref="ProfileWorker"/> - Singleton background service (also registered as IHostedService)</description></item>
/// <item><description><see cref="ProfileCache"/> - Singleton cache for profile state</description></item>
/// <item><description><see cref="ProfileWork"/> - Scoped service for profile synchronization logic</description></item>
/// </list>
/// </remarks>
public static IServiceCollection AddProfileWorker(this IServiceCollection services, IConfiguration configuration)
{
// Configure ProfileWorker options from appsettings.json
@@ -25,10 +43,27 @@ public static class DependencyInjection
}
/// <summary>
/// Validates ProfileWorkerOptions configuration at startup.
/// Validates <see cref="ProfileWorkerOptions"/> configuration at application startup.
/// Ensures IntervalMS is within acceptable bounds to prevent misconfiguration.
/// </summary>
internal class ProfileWorkerOptionsValidator : IValidateOptions<ProfileWorkerOptions>
{
/// <summary>
/// Validates the ProfileWorker options.
/// </summary>
/// <param name="name">The name of the options instance (not used).</param>
/// <param name="options">The options to validate.</param>
/// <returns>
/// <see cref="ValidateOptionsResult.Success"/> if valid,
/// or <see cref="ValidateOptionsResult.Fail"/> with error message if invalid.
/// </returns>
/// <remarks>
/// Validation rules:
/// <list type="bullet">
/// <item><description>IntervalMS must be greater than 0</description></item>
/// <item><description>IntervalMS should be at least 100ms to avoid excessive CPU usage</description></item>
/// </list>
/// </remarks>
public ValidateOptionsResult Validate(string? name, ProfileWorkerOptions options)
{
if (options.IntervalMS <= 0)

View File

@@ -5,19 +5,46 @@ namespace ECMJobRunner.WebCron.ProfileWorker;
/// <summary>
/// Thread-safe cache for storing active profile configurations.
/// Used to track profile state and detect changes in schedule or removal.
/// Uses <see cref="ConcurrentDictionary{TKey, TValue}"/> to track profile state
/// and detect changes in schedule or removal.
/// </summary>
public class ProfileCache
{
private readonly ConcurrentDictionary<string, CfgProfileDto> _cache = new();
/// <summary>
/// Retrieves a profile from the cache by job identifier.
/// </summary>
/// <param name="jobId">The unique job identifier.</param>
/// <returns>The cached profile, or <c>null</c> if not found.</returns>
public CfgProfileDto? Get(string jobId) => _cache.TryGetValue(jobId, out var profile) ? profile : null;
/// <summary>
/// Adds a new profile or updates an existing profile in the cache.
/// </summary>
/// <param name="jobId">The unique job identifier.</param>
/// <param name="profile">The profile configuration to cache.</param>
public void AddOrUpdate(string jobId, CfgProfileDto profile) => _cache[jobId] = profile;
/// <summary>
/// Attempts to remove a profile from the cache.
/// </summary>
/// <param name="jobId">The unique job identifier.</param>
/// <param name="profile">The removed profile, or <c>null</c> if not found.</param>
/// <returns><c>true</c> if the profile was removed; otherwise, <c>false</c>.</returns>
public bool TryRemove(string jobId, out CfgProfileDto? profile) => _cache.TryRemove(jobId, out profile);
/// <summary>
/// Gets all job identifiers currently stored in the cache.
/// </summary>
/// <returns>A collection of job identifiers.</returns>
public IEnumerable<string> GetAllJobIds() => _cache.Keys;
/// <summary>
/// Attempts to retrieve a profile from the cache.
/// </summary>
/// <param name="jobId">The unique job identifier.</param>
/// <param name="profile">The cached profile, or <c>null</c> if not found.</param>
/// <returns><c>true</c> if the profile was found; otherwise, <c>false</c>.</returns>
public bool TryGetValue(string jobId, out CfgProfileDto? profile) => _cache.TryGetValue(jobId, out profile);
}

View File

@@ -5,9 +5,31 @@ using MediatR;
namespace ECMJobRunner.WebCron.ProfileWorker;
/// <summary>
/// Handles the core business logic for synchronizing profiles between the database and Hangfire.
/// Responsible for adding, updating, and removing recurring jobs based on profile configuration.
/// </summary>
/// <param name="Logger">Logger for diagnostic output.</param>
/// <param name="JobManager">Hangfire recurring job manager.</param>
/// <param name="Mediator">MediatR mediator for executing commands and queries.</param>
/// <param name="ProfileCache">Thread-safe cache for tracking profile state.</param>
public class ProfileWork(ILogger<ProfileWork> Logger, IRecurringJobManager JobManager, IMediator Mediator, ProfileCache ProfileCache)
{
/// <summary>
/// Synchronizes active profiles from the database with Hangfire recurring jobs.
/// </summary>
/// <param name="stoppingToken">Cancellation token for graceful shutdown.</param>
/// <remarks>
/// Execution flow:
/// <list type="number">
/// <item><description>Fetches active profiles from database via MediatR query</description></item>
/// <item><description>Removes jobs from Hangfire that no longer exist in the database</description></item>
/// <item><description>Adds or updates jobs with changed schedules</description></item>
/// <item><description>Updates local cache to reflect current state</description></item>
/// </list>
/// Uses HashSet for O(1) job existence checks to optimize performance.
/// </remarks>
/// <returns>A task representing the asynchronous synchronization operation.</returns>
public async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Fetch active profiles from database

View File

@@ -3,6 +3,28 @@ using Microsoft.Extensions.Options;
namespace ECMJobRunner.WebCron.ProfileWorker;
/// <summary>
/// Background service that continuously synchronizes active profiles with Hangfire recurring jobs.
/// Implements health check monitoring to track service status and failure conditions.
/// </summary>
/// <param name="Logger">Logger for diagnostic output.</param>
/// <param name="ScopeFactory">Factory for creating service scopes (required for scoped service resolution).</param>
/// <param name="Options">Configuration options for interval timing.</param>
/// <remarks>
/// This service runs on a configurable interval (default 1 second) and performs the following:
/// <list type="bullet">
/// <item><description>Fetches active profiles from the database</description></item>
/// <item><description>Synchronizes profiles with Hangfire recurring jobs</description></item>
/// <item><description>Tracks health status based on success/failure patterns</description></item>
/// <item><description>Gracefully handles cancellation during application shutdown</description></item>
/// </list>
/// Health states:
/// <list type="bullet">
/// <item><description><b>Healthy</b>: Last successful run within 3x interval</description></item>
/// <item><description><b>Degraded</b>: 1-2 consecutive failures but within time limit</description></item>
/// <item><description><b>Unhealthy</b>: No success for 3x interval or 3+ consecutive failures</description></item>
/// </list>
/// </remarks>
public class ProfileWorker(
ILogger<ProfileWorker> Logger,
IServiceScopeFactory ScopeFactory,
@@ -15,6 +37,19 @@ public class ProfileWorker(
private int _consecutiveFailures = 0;
private Exception? _lastException = null;
/// <summary>
/// Background service execution loop that synchronizes profiles on a configurable interval.
/// </summary>
/// <param name="stoppingToken">Cancellation token for graceful shutdown.</param>
/// <returns>A task representing the background execution.</returns>
/// <remarks>
/// The loop continues until:
/// <list type="bullet">
/// <item><description>Application shutdown is requested (via stoppingToken)</description></item>
/// <item><description>An unhandled exception causes service failure</description></item>
/// </list>
/// Uses scoped services for each iteration to ensure proper lifetime management.
/// </remarks>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Logger.LogInformation("ProfileWorker started");
@@ -61,6 +96,28 @@ public class ProfileWorker(
Logger.LogInformation("ProfileWorker stopped");
}
/// <summary>
/// Performs a health check by evaluating recent success/failure patterns.
/// </summary>
/// <param name="context">Health check context (unused).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// A <see cref="HealthCheckResult"/> indicating the current health status:
/// <list type="bullet">
/// <item><description><see cref="HealthStatus.Healthy"/> - Service running normally</description></item>
/// <item><description><see cref="HealthStatus.Degraded"/> - Recent failures but still operational</description></item>
/// <item><description><see cref="HealthStatus.Unhealthy"/> - No successful run for extended period</description></item>
/// </list>
/// </returns>
/// <remarks>
/// Health determination logic:
/// <list type="number">
/// <item><description><b>Unhealthy</b>: Time since last success exceeds 3x the configured interval</description></item>
/// <item><description><b>Degraded</b>: 1+ consecutive failures within time threshold</description></item>
/// <item><description><b>Healthy</b>: Recent successful run with no failures</description></item>
/// </list>
/// Includes diagnostic data in the result for monitoring and alerting.
/// </remarks>
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)

View File

@@ -2,14 +2,22 @@ namespace ECMJobRunner.WebCron.ProfileWorker;
/// <summary>
/// Configuration options for ProfileWorker background service.
/// Binds to "ProfileWorker" section in appsettings.json.
/// </summary>
public class ProfileWorkerOptions
{
/// <summary>
/// Configuration section name for binding options.
/// </summary>
public const string SectionName = "ProfileWorker";
/// <summary>
/// Interval in milliseconds between profile synchronization checks.
/// Default: 1000ms (1 second)
/// Default: 1000ms (1 second).
/// </summary>
/// <remarks>
/// Lower values increase responsiveness but consume more resources.
/// Recommended range: 1000-5000ms.
/// </remarks>
public int IntervalMS { get; set; } = 1000;
}