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:
@@ -8,6 +8,11 @@ namespace ECMJobRunner.WebCron
|
||||
/// </summary>
|
||||
public class AllowAllDashboardAuthorizationFilter : IDashboardAuthorizationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the current user is authorized to access the Hangfire Dashboard.
|
||||
/// </summary>
|
||||
/// <param name="context">The Hangfire dashboard context containing request information.</param>
|
||||
/// <returns>Always returns <c>true</c> to allow all users (development only).</returns>
|
||||
public bool Authorize(DashboardContext context)
|
||||
{
|
||||
// Allow all users - FOR DEVELOPMENT ONLY
|
||||
|
||||
@@ -4,13 +4,30 @@ using MediatR;
|
||||
|
||||
namespace ECMJobRunner.WebCron.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for profile DTOs to support Hangfire job operations.
|
||||
/// </summary>
|
||||
public static class DtoExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a unique Hangfire job identifier for a profile.
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile configuration DTO.</param>
|
||||
/// <returns>A unique job identifier in the format "profile-{Id}-{normalized-name}".</returns>
|
||||
/// <example>
|
||||
/// Example: profile with Id=123 and ProfileName="Import Data"
|
||||
/// returns "profile-123-import_data"
|
||||
/// </example>
|
||||
public static string JobId(this CfgProfileDto profile)
|
||||
{
|
||||
return $"profile-{profile.Id}-{profile.ProfileName.Replace(' ', '_').ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a profile DTO to a DEX job batch command.
|
||||
/// </summary>
|
||||
/// <param name="profile">The profile configuration DTO.</param>
|
||||
/// <returns>A <see cref="TriggeringDEXJobBatchCommand"/> ready to execute the profile job.</returns>
|
||||
public static TriggeringDEXJobBatchCommand ToJob(this CfgProfileDto profile)
|
||||
{
|
||||
return new TriggeringDEXJobBatchCommand
|
||||
|
||||
@@ -4,10 +4,32 @@ using System.Text;
|
||||
namespace ECMJobRunner.WebCron.HealthCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Generates HTML representation of health check reports.
|
||||
/// Generates HTML representation of ASP.NET Core health check reports.
|
||||
/// Produces a Bootstrap 5-based UI with auto-refresh, status indicators, and detailed metrics.
|
||||
/// </summary>
|
||||
public static class HealthCheckHtmlGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a complete HTML page for displaying health check results.
|
||||
/// </summary>
|
||||
/// <param name="report">The health check report containing service status information.</param>
|
||||
/// <returns>A complete HTML document string ready to be sent as HTTP response.</returns>
|
||||
/// <remarks>
|
||||
/// The generated UI includes:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Navigation links to Hangfire, Serilog.UI, and health endpoints</description></item>
|
||||
/// <item><description>Overall health status with color-coded badge</description></item>
|
||||
/// <item><description>Summary card with total duration and check count</description></item>
|
||||
/// <item><description>Individual check cards with detailed metrics and exception info</description></item>
|
||||
/// <item><description>Auto-refresh countdown (10 seconds)</description></item>
|
||||
/// </list>
|
||||
/// External dependencies:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Bootstrap 5.3.0 (CDN)</description></item>
|
||||
/// <item><description>/css/health-ui.css (custom styles)</description></item>
|
||||
/// <item><description>/js/health-ui.js (auto-refresh logic)</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static string Generate(HealthReport report)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
@@ -47,6 +69,10 @@ public static class HealthCheckHtmlGenerator
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the navigation menu with links to application dashboards.
|
||||
/// </summary>
|
||||
/// <param name="sb">The StringBuilder to append HTML to.</param>
|
||||
private static void AppendNavigation(StringBuilder sb)
|
||||
{
|
||||
sb.Append(@"
|
||||
@@ -66,6 +92,11 @@ public static class HealthCheckHtmlGenerator
|
||||
sb.AppendLine(" </div>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the overall status header showing aggregate health and timestamp.
|
||||
/// </summary>
|
||||
/// <param name="sb">The StringBuilder to append HTML to.</param>
|
||||
/// <param name="report">The health check report.</param>
|
||||
private static void AppendOverallStatus(StringBuilder sb, HealthReport report)
|
||||
{
|
||||
var statusClass = GetStatusClass(report.Status);
|
||||
@@ -91,6 +122,11 @@ public static class HealthCheckHtmlGenerator
|
||||
<div class=""col-md-4"">");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the summary card displaying aggregate metrics (status, duration, check count).
|
||||
/// </summary>
|
||||
/// <param name="sb">The StringBuilder to append HTML to.</param>
|
||||
/// <param name="report">The health check report.</param>
|
||||
private static void AppendSummaryCard(StringBuilder sb, HealthReport report)
|
||||
{
|
||||
var statusClass = GetStatusClass(report.Status);
|
||||
@@ -122,6 +158,11 @@ public static class HealthCheckHtmlGenerator
|
||||
<div class=""col-md-8"">");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends individual health check result cards with detailed metrics.
|
||||
/// </summary>
|
||||
/// <param name="sb">The StringBuilder to append HTML to.</param>
|
||||
/// <param name="report">The health check report containing check entries.</param>
|
||||
private static void AppendIndividualChecks(StringBuilder sb, HealthReport report)
|
||||
{
|
||||
foreach (var entry in report.Entries)
|
||||
@@ -175,6 +216,11 @@ public static class HealthCheckHtmlGenerator
|
||||
</div>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a detail table for additional health check data (e.g., timestamps, counters).
|
||||
/// </summary>
|
||||
/// <param name="sb">The StringBuilder to append HTML to.</param>
|
||||
/// <param name="data">Dictionary of custom data provided by the health check.</param>
|
||||
private static void AppendCheckDetails(StringBuilder sb, IReadOnlyDictionary<string, object> data)
|
||||
{
|
||||
sb.AppendLine(@"
|
||||
@@ -198,6 +244,10 @@ public static class HealthCheckHtmlGenerator
|
||||
</div>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the auto-refresh indicator widget with countdown timer.
|
||||
/// </summary>
|
||||
/// <param name="sb">The StringBuilder to append HTML to.</param>
|
||||
private static void AppendRefreshInfo(StringBuilder sb)
|
||||
{
|
||||
sb.AppendLine(@"
|
||||
@@ -208,6 +258,11 @@ public static class HealthCheckHtmlGenerator
|
||||
sb.AppendLine(@" </div>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a health status to the corresponding CSS class name for styling.
|
||||
/// </summary>
|
||||
/// <param name="status">The health status.</param>
|
||||
/// <returns>CSS class name (e.g., "status-healthy", "status-degraded").</returns>
|
||||
private static string GetStatusClass(HealthStatus status)
|
||||
{
|
||||
return status switch
|
||||
@@ -219,6 +274,11 @@ public static class HealthCheckHtmlGenerator
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a health status to the corresponding Bootstrap badge CSS class.
|
||||
/// </summary>
|
||||
/// <param name="status">The health status.</param>
|
||||
/// <returns>Badge CSS class name (e.g., "badge-healthy", "badge-degraded").</returns>
|
||||
private static string GetBadgeClass(HealthStatus status)
|
||||
{
|
||||
return status switch
|
||||
@@ -230,6 +290,11 @@ public static class HealthCheckHtmlGenerator
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a health status to the corresponding emoji icon.
|
||||
/// </summary>
|
||||
/// <param name="status">The health status.</param>
|
||||
/// <returns>Unicode emoji character (✅ for healthy, ⚠️ for degraded, ❌ for unhealthy).</returns>
|
||||
private static string GetStatusIcon(HealthStatus status)
|
||||
{
|
||||
return status switch
|
||||
@@ -241,6 +306,19 @@ public static class HealthCheckHtmlGenerator
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats health check data values for display.
|
||||
/// Applies special formatting for common types (DateTime, TimeSpan).
|
||||
/// </summary>
|
||||
/// <param name="value">The data value to format.</param>
|
||||
/// <returns>
|
||||
/// Formatted string representation:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see cref="DateTime"/> → "yyyy-MM-dd HH:mm:ss"</description></item>
|
||||
/// <item><description><see cref="TimeSpan"/> → "{seconds}s"</description></item>
|
||||
/// <item><description>Other types → ToString() or "N/A" if null</description></item>
|
||||
/// </list>
|
||||
/// </returns>
|
||||
private static string FormatDataValue(object? value)
|
||||
{
|
||||
if (value == null)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user