diff --git a/.appveyor.yml b/.appveyor.yml index ff471fb..c49aaa2 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,7 +1,7 @@ version: '{build}' pull_requests: do_not_increment_build_number: true -image: Visual Studio 2019 +image: Visual Studio 2022 nuget: disable_publish_on_pr: true build_script: diff --git a/.travis.yml b/.travis.yml index eca6876..c59b90f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: csharp mono: none -dotnet: 5.0 +dotnet: 7.0 dist: xenial env: global: diff --git a/src/Webenable.Hangfire.Contrib/AutoScheduleAttribute.cs b/src/Webenable.Hangfire.Contrib/AutoScheduleAttribute.cs index fe25bea..f6c7a0e 100644 --- a/src/Webenable.Hangfire.Contrib/AutoScheduleAttribute.cs +++ b/src/Webenable.Hangfire.Contrib/AutoScheduleAttribute.cs @@ -1,34 +1,33 @@ using System; -namespace Webenable.Hangfire.Contrib +namespace Webenable.Hangfire.Contrib; + +/// +/// Specifies that the job should be automatically scheduled in Hangfire +/// using the specified and . +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class AutoScheduleAttribute : Attribute { /// /// Specifies that the job should be automatically scheduled in Hangfire - /// using the specified and . + /// using the specified and . /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class AutoScheduleAttribute : Attribute + /// The name of the method to invoke the job with. + /// The CRON expression to schedule the job with. + public AutoScheduleAttribute(string methodName, string cronExpression) { - /// - /// Specifies that the job should be automatically scheduled in Hangfire - /// using the specified and . - /// - /// The name of the method to invoke the job with. - /// The CRON expression to schedule the job with. - public AutoScheduleAttribute(string methodName, string cronExpression) - { - MethodName = methodName; - CronExpression = cronExpression; - } + MethodName = methodName; + CronExpression = cronExpression; + } - /// - /// Gets the name of the method to invoke the job with. - /// - public string MethodName { get; } + /// + /// Gets the name of the method to invoke the job with. + /// + public string MethodName { get; } - /// - /// Gets the CRON expression to schedule the job with. - /// - public string CronExpression { get; } - } + /// + /// Gets the CRON expression to schedule the job with. + /// + public string CronExpression { get; } } diff --git a/src/Webenable.Hangfire.Contrib/Crons.cs b/src/Webenable.Hangfire.Contrib/Crons.cs index 9dcf1e7..ff17199 100644 --- a/src/Webenable.Hangfire.Contrib/Crons.cs +++ b/src/Webenable.Hangfire.Contrib/Crons.cs @@ -1,38 +1,37 @@ -namespace Webenable.Hangfire.Contrib +namespace Webenable.Hangfire.Contrib; + +/// +/// Contains constant CRON expressions for convenience use in . +/// +public static class Crons { /// - /// Contains constant CRON expressions for convenience use in . + /// Every minute. /// - public static class Crons - { - /// - /// Every minute. - /// - public const string Minutely = "* * * * *"; + public const string Minutely = "* * * * *"; - /// - /// Every hour. - /// - public const string Hourly = "0 * * * *"; + /// + /// Every hour. + /// + public const string Hourly = "0 * * * *"; - /// - /// Every day at 00:00. - /// - public const string Daily = "0 0 * * *"; + /// + /// Every day at 00:00. + /// + public const string Daily = "0 0 * * *"; - /// - /// Every Monday at 00:00. - /// - public const string Weekly = "0 0 * * 1"; + /// + /// Every Monday at 00:00. + /// + public const string Weekly = "0 0 * * 1"; - /// - /// Every first day of the month at 00:00. - /// - public const string Monthly = "0 0 1 * *"; + /// + /// Every first day of the month at 00:00. + /// + public const string Monthly = "0 0 1 * *"; - /// - /// Every year on January 1st at 00:00. - /// - public const string Yearly = "0 0 1 1 *"; - } + /// + /// Every year on January 1st at 00:00. + /// + public const string Yearly = "0 0 1 1 *"; } diff --git a/src/Webenable.Hangfire.Contrib/HangfireContribOptions.cs b/src/Webenable.Hangfire.Contrib/HangfireContribOptions.cs index e2c57e6..5a87ebc 100644 --- a/src/Webenable.Hangfire.Contrib/HangfireContribOptions.cs +++ b/src/Webenable.Hangfire.Contrib/HangfireContribOptions.cs @@ -2,55 +2,54 @@ using System.Reflection; using Microsoft.AspNetCore.Http; -namespace Webenable.Hangfire.Contrib +namespace Webenable.Hangfire.Contrib; + +/// +/// Defines options for the Hangfire contrib extensions. +/// +public class HangfireContribOptions { /// - /// Defines options for the Hangfire contrib extensions. + /// Gets or sets a value indicating whether the Hangfire background server should be enabled. + /// + public bool EnableServer { get; set; } + + /// + /// Gets or sets the assemblies used to scan for job types. + /// By default the entry assembly of the application. + /// + public Assembly[] ScanningAssemblies { get; set; } = Array.Empty(); + + /// + /// Gets or sets options for the Hangfire dashboard. + /// + public DasbhoardOptions Dasbhoard { get; set; } = new DasbhoardOptions(); + + /// + /// Defines options for the Hangfire dashboard. /// - public class HangfireContribOptions + public class DasbhoardOptions { /// - /// Gets or sets a value indicating whether the Hangfire background server should be enabled. + /// Gets or sets a value indicating whether the Hangfire dashboard should be enabled. /// - public bool EnableServer { get; set; } + public bool Enabled { get; set; } /// - /// Gets or sets the assemblies used to scan for job types. - /// By default the entry assembly of the application. + /// Gets or sets a value indicating whether the Hangfire dashboard IP-based authorization filter should be enabled. /// - public Assembly[] ScanningAssemblies { get; set; } = Array.Empty(); + public bool EnableAuthorization { get; set; } /// - /// Gets or sets options for the Hangfire dashboard. + /// Gets or sets a callback which gets invoked when authorizing a dashboard request. + /// If not specified, the default authorization polcy is IP-based using when IP-addresses are specified. /// - public DasbhoardOptions Dasbhoard { get; set; } = new DasbhoardOptions(); + public Func? AuthorizationCallback { get; set; } /// - /// Defines options for the Hangfire dashboard. + /// Gets or sets the collection of IP-addresses which are allowed to access the Hangfire dashboard. + /// This is the default authorization policy. /// - public class DasbhoardOptions - { - /// - /// Gets or sets a value indicating whether the Hangfire dashboard should be enabled. - /// - public bool Enabled { get; set; } - - /// - /// Gets or sets a value indicating whether the Hangfire dashboard IP-based authorization filter should be enabled. - /// - public bool EnableAuthorization { get; set; } - - /// - /// Gets or sets a callback which gets invoked when authorizing a dashboard request. - /// If not specified, the default authorization polcy is IP-based using when IP-addresses are specified. - /// - public Func? AuthorizationCallback { get; set; } - - /// - /// Gets or sets the collection of IP-addresses which are allowed to access the Hangfire dashboard. - /// This is the default authorization policy. - /// - public string[]? AllowedIps { get; set; } - } + public string[]? AllowedIps { get; set; } } } diff --git a/src/Webenable.Hangfire.Contrib/HangfireExtensions.cs b/src/Webenable.Hangfire.Contrib/HangfireExtensions.cs index d9066f2..336956b 100644 --- a/src/Webenable.Hangfire.Contrib/HangfireExtensions.cs +++ b/src/Webenable.Hangfire.Contrib/HangfireExtensions.cs @@ -3,29 +3,27 @@ using Microsoft.Extensions.Logging; using Webenable.Hangfire.Contrib.Internal; -namespace Webenable.Hangfire.Contrib +namespace Webenable.Hangfire.Contrib; + +/// +/// Extensions for logging in Hangfire jobs. +/// +public static class HangfireExtensions { /// - /// Extensions for logging in Hangfire jobs. + /// Begins a logical operation scope for the given within the scope of a job. /// - public static class HangfireExtensions - { - /// - /// Begins a logical operation scope for the given within the scope of a job. - /// - /// The instance. - /// The instance for the job. - public static IDisposable BeginJobScope(this ILogger logger, PerformContext performContext) => - performContext != null ? logger.BeginScope(new PerformContextWrapper(performContext)) : NoopDisposable.Instance; + /// The instance. + /// The instance for the job. + public static IDisposable BeginJobScope(this ILogger logger, PerformContext performContext) => + performContext != null ? logger.BeginScope(new PerformContextWrapper(performContext))! : NoopDisposable.Instance; + private class NoopDisposable : IDisposable + { + public static NoopDisposable Instance = new(); - private class NoopDisposable : IDisposable + public void Dispose() { - public static NoopDisposable Instance = new(); - - public void Dispose() - { - } } } } diff --git a/src/Webenable.Hangfire.Contrib/HangfireJob.cs b/src/Webenable.Hangfire.Contrib/HangfireJob.cs index 3afd99b..3d9675d 100644 --- a/src/Webenable.Hangfire.Contrib/HangfireJob.cs +++ b/src/Webenable.Hangfire.Contrib/HangfireJob.cs @@ -5,92 +5,91 @@ using Hangfire.Server; using Microsoft.Extensions.Logging; -namespace Webenable.Hangfire.Contrib +namespace Webenable.Hangfire.Contrib; + +/// +/// Base class for Hangfire jobs which provides auto-scheduling and logging. +/// +public abstract class HangfireJob { /// - /// Base class for Hangfire jobs which provides auto-scheduling and logging. + /// Initializes the base class with the specified . /// - public abstract class HangfireJob + /// The logger factory to use for logging. + protected HangfireJob(ILoggerFactory loggerFactory) { - /// - /// Initializes the base class with the specified . - /// - /// The logger factory to use for logging. - protected HangfireJob(ILoggerFactory loggerFactory) - { - Logger = loggerFactory.CreateLogger(GetType()); - } - - /// - /// Gets the instance for this job. - /// - protected ILogger Logger { get; } + Logger = loggerFactory.CreateLogger(GetType()); + } - /// - /// Gets the perform context instance of this job. - /// May be null. - /// - protected PerformContext? PerformContext { get; private set; } + /// + /// Gets the instance for this job. + /// + protected ILogger Logger { get; } - /// - /// Executes the job. - /// - /// - /// The context in which the job is performed. Populated by Hangfire. - /// It is safe to pass null in unit tests or other manual scenarios. - /// - /// - /// The . Populated by Hangfire. - /// It is safe to pass null in unit tests or other manual scenarios, - /// a new instance of will be created in that case. - /// - public async Task ExecuteAsync(PerformContext performContext, IJobCancellationToken cancellationToken) - { - PerformContext = performContext; - var jobId = performContext?.BackgroundJob?.Id; + /// + /// Gets the perform context instance of this job. + /// May be null. + /// + protected PerformContext? PerformContext { get; private set; } - cancellationToken?.ThrowIfCancellationRequested(); + /// + /// Executes the job. + /// + /// + /// The context in which the job is performed. Populated by Hangfire. + /// It is safe to pass null in unit tests or other manual scenarios. + /// + /// + /// The . Populated by Hangfire. + /// It is safe to pass null in unit tests or other manual scenarios, + /// a new instance of will be created in that case. + /// + public async Task ExecuteAsync(PerformContext performContext, IJobCancellationToken cancellationToken) + { + PerformContext = performContext; + var jobId = performContext?.BackgroundJob?.Id; - IDisposable? jobScope = null; - IDisposable? performContextScope = null; + cancellationToken?.ThrowIfCancellationRequested(); - // Perform context is optional, e.g. may be null in unit tests - if (performContext != null) - { - if (!string.IsNullOrEmpty(jobId)) - { - jobScope = Logger.BeginScope("Job {JobId}", jobId); - } + IDisposable? jobScope = null; + IDisposable? performContextScope = null; - performContextScope = Logger.BeginJobScope(performContext); - } - - Logger.LogDebug("Starting job {JobId}", jobId); - try - { - await ExecuteCoreAsync(cancellationToken ?? new JobCancellationToken(false)); - Logger.LogDebug("Finished job {JobId}", jobId); - } - catch (Exception ex) + // Perform context is optional, e.g. may be null in unit tests + if (performContext != null) + { + if (!string.IsNullOrEmpty(jobId)) { - Logger.LogError(ex, "Failed executing job {JobId}/{JobName}: {JobException}", jobId, GetType().Name, ex.ToStringDemystified()); - throw; + jobScope = Logger.BeginScope("Job {JobId}", jobId); } - performContextScope?.Dispose(); - jobScope?.Dispose(); + performContextScope = Logger.BeginJobScope(performContext); } - /// - /// Executes the inner logic of the job. - /// - /// The . - protected abstract Task ExecuteCoreAsync(IJobCancellationToken cancellationToken); + Logger.LogDebug("Starting job {JobId}", jobId); + try + { + await ExecuteCoreAsync(cancellationToken ?? new JobCancellationToken(false)); + Logger.LogDebug("Finished job {JobId}", jobId); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed executing job {JobId}/{JobName}: {JobException}", jobId, GetType().Name, ex.ToStringDemystified()); + throw; + } - /// - /// Gets the schedule for automatically scheduled jobs. - /// Default is null which means that the job is not automatically scheduled. - /// - public virtual string? Schedule => null; + performContextScope?.Dispose(); + jobScope?.Dispose(); } + + /// + /// Executes the inner logic of the job. + /// + /// The . + protected abstract Task ExecuteCoreAsync(IJobCancellationToken cancellationToken); + + /// + /// Gets the schedule for automatically scheduled jobs. + /// Default is null which means that the job is not automatically scheduled. + /// + public virtual string? Schedule => null; } diff --git a/src/Webenable.Hangfire.Contrib/HangfireServiceCollectionExtensions.cs b/src/Webenable.Hangfire.Contrib/HangfireServiceCollectionExtensions.cs index 7a86cd3..716249a 100644 --- a/src/Webenable.Hangfire.Contrib/HangfireServiceCollectionExtensions.cs +++ b/src/Webenable.Hangfire.Contrib/HangfireServiceCollectionExtensions.cs @@ -7,45 +7,44 @@ using Microsoft.Extensions.Logging; using Webenable.Hangfire.Contrib.Internal; -namespace Webenable.Hangfire.Contrib +namespace Webenable.Hangfire.Contrib; + +/// +/// Extensions for Hangfire contrib. +/// +public static class HangfireServiceCollectionExtensions { /// - /// Extensions for Hangfire contrib. + /// Adds Hangfire contrib extensions. /// - public static class HangfireServiceCollectionExtensions - { - /// - /// Adds Hangfire contrib extensions. - /// - public static IServiceCollection AddHangfireContrib(this IServiceCollection services) => AddHangfireContrib(services, configAction: null); + public static IServiceCollection AddHangfireContrib(this IServiceCollection services) => AddHangfireContrib(services, configAction: null); - /// - /// Adds Hangfire contrib extensions and configures Hangfire with the specified action. - /// - public static IServiceCollection AddHangfireContrib(this IServiceCollection services, Action? configAction) + /// + /// Adds Hangfire contrib extensions and configures Hangfire with the specified action. + /// + public static IServiceCollection AddHangfireContrib(this IServiceCollection services, Action? configAction) + { + services.AddHangfire(c => { - services.AddHangfire(c => - { - c.UseConsole(); - configAction?.Invoke(c); - }); + c.UseConsole(); + configAction?.Invoke(c); + }); - // Add Hangfire options instances to the ASP.NET options infrastructure - services.Configure(o => { }); - services.Configure(o => { }); + // Add Hangfire options instances to the ASP.NET options infrastructure + services.Configure(o => { }); + services.Configure(o => { }); - // Configure default options for Hangfire.Contrib - services.Configure(o => - { - o.EnableServer = true; - o.Dasbhoard.Enabled = true; - o.ScanningAssemblies = new[] { Assembly.GetEntryAssembly()! }; - }); + // Configure default options for Hangfire.Contrib + services.Configure(o => + { + o.EnableServer = true; + o.Dasbhoard.Enabled = true; + o.ScanningAssemblies = new[] { Assembly.GetEntryAssembly()! }; + }); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - return services; - } + return services; } } diff --git a/src/Webenable.Hangfire.Contrib/Internal/DashboardAuthorizationFilter.cs b/src/Webenable.Hangfire.Contrib/Internal/DashboardAuthorizationFilter.cs index a8ab9ed..1e8a3df 100644 --- a/src/Webenable.Hangfire.Contrib/Internal/DashboardAuthorizationFilter.cs +++ b/src/Webenable.Hangfire.Contrib/Internal/DashboardAuthorizationFilter.cs @@ -6,90 +6,89 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Webenable.Hangfire.Contrib.Internal +namespace Webenable.Hangfire.Contrib.Internal; + +public class DashboardAuthorizationFilter : IDashboardAuthorizationFilter { - public class DashboardAuthorizationFilter : IDashboardAuthorizationFilter + private readonly IWebHostEnvironment? _environment; + private readonly string[]? _allowedIps; + private readonly ILogger _logger; + private readonly Func? _authorizationCallback; + + public DashboardAuthorizationFilter(Func authorizationCallback, ILoggerFactory loggerFactory) { - private readonly IWebHostEnvironment? _environment; - private readonly string[]? _allowedIps; - private readonly ILogger _logger; - private readonly Func? _authorizationCallback; + _authorizationCallback = authorizationCallback; + _logger = loggerFactory.CreateLogger(); + } - public DashboardAuthorizationFilter(Func authorizationCallback, ILoggerFactory loggerFactory) - { - _authorizationCallback = authorizationCallback; - _logger = loggerFactory.CreateLogger(); - } + public DashboardAuthorizationFilter(IWebHostEnvironment environment, string[] allowedIps, ILoggerFactory loggerFactory) + { + _environment = environment; + _allowedIps = allowedIps; + _logger = loggerFactory.CreateLogger(); + } + + public bool Authorize(DashboardContext context) + { + var httpContext = context.GetHttpContext(); - public DashboardAuthorizationFilter(IWebHostEnvironment environment, string[] allowedIps, ILoggerFactory loggerFactory) + // Invoke the custom specified authorization callback if specified. + // Otherwise execute IP-based authorization. + if (_authorizationCallback != null) { - _environment = environment; - _allowedIps = allowedIps; - _logger = loggerFactory.CreateLogger(); + return InvokeAuthorizationCallback(httpContext); } - public bool Authorize(DashboardContext context) + return AuthorizeIpAddress(httpContext); + } + + private bool InvokeAuthorizationCallback(HttpContext httpContext) + { + if (_authorizationCallback?.Invoke(httpContext) == true) { - var httpContext = context.GetHttpContext(); + _logger.LogDebug("Grant access to Hangfire dashboard"); + return true; + } - // Invoke the custom specified authorization callback if specified. - // Otherwise execute IP-based authorization. - if (_authorizationCallback != null) - { - return InvokeAuthorizationCallback(httpContext); - } + _logger.LogWarning("Deny access to Hangfire dashboard"); + return false; + } - return AuthorizeIpAddress(httpContext); + private bool AuthorizeIpAddress(HttpContext httpContext) + { + if (_environment?.IsDevelopment() == true) + { + // Always allow requests in development environment. + _logger.LogDebug("Grant access to Hangfire dashboard in development environment"); + return true; } - private bool InvokeAuthorizationCallback(HttpContext httpContext) + if (_allowedIps == null || _allowedIps.Length == 0) { - if (_authorizationCallback?.Invoke(httpContext) == true) - { - _logger.LogDebug("Grant access to Hangfire dashboard"); - return true; - } - - _logger.LogWarning("Deny access to Hangfire dashboard"); + _logger.LogWarning("Deny access to dashboard: no allowed IP addresses configured"); return false; } - private bool AuthorizeIpAddress(HttpContext httpContext) + // Resolve remote IP addresses from the forwarded headers as well as the default header. + var ips = httpContext + .Request + .Headers["X-Forwarded-For"] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Append(httpContext.Connection.RemoteIpAddress?.ToString()) + .Select(r => r?.Trim()) + .Where(x => !string.IsNullOrEmpty(x)); + + foreach (var ip in ips) { - if (_environment?.IsDevelopment() == true) + if (_allowedIps.Contains(ip)) { - // Always allow requests in development environment. - _logger.LogDebug("Grant access to Hangfire dashboard in development environment"); + _logger.LogDebug("Grant access to Hangfire dashboard for IP-address {IpAddress}", ip); return true; } - - if (_allowedIps == null || _allowedIps.Length == 0) - { - _logger.LogWarning("Deny access to dashboard: no allowed IP addresses configured"); - return false; - } - - // Resolve remote IP addresses from the forwarded headers as well as the default header. - var ips = httpContext - .Request - .Headers["X-Forwarded-For"] - .ToString() - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Append(httpContext.Connection.RemoteIpAddress?.ToString()) - .Select(r => r?.Trim()) - .Where(x => !string.IsNullOrEmpty(x)); - - foreach (var ip in ips) - { - if (_allowedIps.Contains(ip)) - { - _logger.LogDebug("Grant access to Hangfire dashboard for IP-address {IpAddress}", ip); - return true; - } - } - - _logger.LogWarning("Deny access to Hangfire dashboard for IP-addresses {IpAddresses}", string.Join(", ", ips)); - return false; } + + _logger.LogWarning("Deny access to Hangfire dashboard for IP-addresses {IpAddresses}", string.Join(", ", ips)); + return false; } } diff --git a/src/Webenable.Hangfire.Contrib/Internal/HangfireContribStartupFilter.cs b/src/Webenable.Hangfire.Contrib/Internal/HangfireContribStartupFilter.cs index 913062e..c77d734 100644 --- a/src/Webenable.Hangfire.Contrib/Internal/HangfireContribStartupFilter.cs +++ b/src/Webenable.Hangfire.Contrib/Internal/HangfireContribStartupFilter.cs @@ -8,127 +8,126 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Webenable.Hangfire.Contrib.Internal +namespace Webenable.Hangfire.Contrib.Internal; + +public class HangfireContribStartupFilter : IStartupFilter { - public class HangfireContribStartupFilter : IStartupFilter + private readonly HangfireContribOptions _contribOptions; + private readonly BackgroundJobServerOptions _backgroundJobServerOptions; + private readonly DashboardOptions _dashboardOptions; + private readonly IRecurringJobManager _recurringJobManager; + private readonly ILogger _logger; + + public HangfireContribStartupFilter( + IOptions options, + IOptions backgroundJobServerOptions, + IOptions dashboardOptions, + IRecurringJobManager recurringJobManager, + ILogger logger) { - private readonly HangfireContribOptions _contribOptions; - private readonly BackgroundJobServerOptions _backgroundJobServerOptions; - private readonly DashboardOptions _dashboardOptions; - private readonly IRecurringJobManager _recurringJobManager; - private readonly ILogger _logger; + _contribOptions = options.Value; + _backgroundJobServerOptions = backgroundJobServerOptions.Value; + _dashboardOptions = dashboardOptions.Value; + _recurringJobManager = recurringJobManager; + _logger = logger; + } - public HangfireContribStartupFilter( - IOptions options, - IOptions backgroundJobServerOptions, - IOptions dashboardOptions, - IRecurringJobManager recurringJobManager, - ILogger logger) + public Action Configure(Action next) => + app => { - _contribOptions = options.Value; - _backgroundJobServerOptions = backgroundJobServerOptions.Value; - _dashboardOptions = dashboardOptions.Value; - _recurringJobManager = recurringJobManager; - _logger = logger; - } - - public Action Configure(Action next) => - app => + if (_contribOptions.EnableServer) { - if (_contribOptions.EnableServer) - { - _logger.LogInformation("Enabling Hangfire server"); - app.UseHangfireServer(_backgroundJobServerOptions); - } + _logger.LogInformation("Enabling Hangfire server"); + app.UseHangfireServer(_backgroundJobServerOptions); + } - if (_contribOptions.Dasbhoard.Enabled) - { - ConfigureDashboard(app); - } + if (_contribOptions.Dasbhoard.Enabled) + { + ConfigureDashboard(app); + } - RegisterJobs(app); + RegisterJobs(app); - next(app); - }; + next(app); + }; - private void ConfigureDashboard(IApplicationBuilder app) + private void ConfigureDashboard(IApplicationBuilder app) + { + _logger.LogInformation("Enabling Hangfire dashboard"); + var dashboardOptions = _dashboardOptions ?? new DashboardOptions(); + if (_contribOptions.Dasbhoard.EnableAuthorization) { - _logger.LogInformation("Enabling Hangfire dashboard"); - var dashboardOptions = _dashboardOptions ?? new DashboardOptions(); - if (_contribOptions.Dasbhoard.EnableAuthorization) - { - var loggerFactory = app.ApplicationServices.GetRequiredService(); + var loggerFactory = app.ApplicationServices.GetRequiredService(); - DashboardAuthorizationFilter dashboardAuthorizationFilter; - if (_contribOptions.Dasbhoard.AuthorizationCallback != null) - { - _logger.LogInformation("Configuring Hangfire dashboard authorization with custom callback"); - dashboardAuthorizationFilter = new DashboardAuthorizationFilter(_contribOptions.Dasbhoard.AuthorizationCallback, loggerFactory); - } - else if (_contribOptions.Dasbhoard.AllowedIps?.Length > 0) - { - _logger.LogInformation("Configuring Hangfire IP-based dashboard authorization"); - dashboardAuthorizationFilter = new DashboardAuthorizationFilter(app.ApplicationServices.GetRequiredService(), _contribOptions.Dasbhoard.AllowedIps, loggerFactory); - } - else - { - throw new InvalidOperationException("No custom authorization callback or allowed IP-addresses configured for Hangfire dashboard authorization."); - } - - dashboardOptions.Authorization = new[] { dashboardAuthorizationFilter }; + DashboardAuthorizationFilter dashboardAuthorizationFilter; + if (_contribOptions.Dasbhoard.AuthorizationCallback != null) + { + _logger.LogInformation("Configuring Hangfire dashboard authorization with custom callback"); + dashboardAuthorizationFilter = new DashboardAuthorizationFilter(_contribOptions.Dasbhoard.AuthorizationCallback, loggerFactory); + } + else if (_contribOptions.Dasbhoard.AllowedIps?.Length > 0) + { + _logger.LogInformation("Configuring Hangfire IP-based dashboard authorization"); + dashboardAuthorizationFilter = new DashboardAuthorizationFilter(app.ApplicationServices.GetRequiredService(), _contribOptions.Dasbhoard.AllowedIps, loggerFactory); + } + else + { + throw new InvalidOperationException("No custom authorization callback or allowed IP-addresses configured for Hangfire dashboard authorization."); } - app.UseHangfireDashboard(options: dashboardOptions); + dashboardOptions.Authorization = new[] { dashboardAuthorizationFilter }; } - private static readonly MethodInfo HangfireJobExecuteAsyncMethod = typeof(HangfireJob).GetMethod(nameof(HangfireJob.ExecuteAsync))!; + app.UseHangfireDashboard(options: dashboardOptions); + } + + private static readonly MethodInfo HangfireJobExecuteAsyncMethod = typeof(HangfireJob).GetMethod(nameof(HangfireJob.ExecuteAsync))!; - private void RegisterJobs(IApplicationBuilder app) + private void RegisterJobs(IApplicationBuilder app) + { + using var scope = app.ApplicationServices.CreateScope(); + var sp = scope.ServiceProvider; + var hangfireJobType = typeof(HangfireJob); + foreach (var assembly in _contribOptions.ScanningAssemblies) { - using var scope = app.ApplicationServices.CreateScope(); - var sp = scope.ServiceProvider; - var hangfireJobType = typeof(HangfireJob); - foreach (var assembly in _contribOptions.ScanningAssemblies) + foreach (var candidate in assembly.ExportedTypes) { - foreach (var candidate in assembly.ExportedTypes) + if (candidate.IsAbstract) { - if (candidate.IsAbstract) - { - // Skip abstract types - continue; - } + // Skip abstract types + continue; + } - if (hangfireJobType.IsAssignableFrom(candidate) && candidate != hangfireJobType) + if (hangfireJobType.IsAssignableFrom(candidate) && candidate != hangfireJobType) + { + try { - try + var jobInstance = (HangfireJob)ActivatorUtilities.CreateInstance(sp, candidate); + if (!string.IsNullOrEmpty(jobInstance.Schedule)) { - var jobInstance = (HangfireJob)ActivatorUtilities.CreateInstance(sp, candidate); - if (!string.IsNullOrEmpty(jobInstance.Schedule)) - { - _logger.LogInformation("Auto-scheduling job {JobName} with schedule {JobSchedule}", candidate.Name, jobInstance.Schedule); - _recurringJobManager.AddOrUpdate( - candidate.Name, - new Job(candidate, HangfireJobExecuteAsyncMethod, null, null), - jobInstance.Schedule); - } - else - { - _logger.LogDebug("Job {JobName} auto-scheduling is disabled", candidate.Name); - } + _logger.LogInformation("Auto-scheduling job {JobName} with schedule {JobSchedule}", candidate.Name, jobInstance.Schedule); + _recurringJobManager.AddOrUpdate( + candidate.Name, + new Job(candidate, HangfireJobExecuteAsyncMethod, null, null), + jobInstance.Schedule); } - catch (Exception ex) + else { - _logger.LogError(ex, "Unable to activate job {JobName}: {ExceptionMessage}", candidate.Name, ex.Message); + _logger.LogDebug("Job {JobName} auto-scheduling is disabled", candidate.Name); } } - else + catch (Exception ex) { - var scheduleAttr = candidate.GetCustomAttribute(); - if (scheduleAttr != null) - { - _logger.LogInformation("Auto-scheduling job {JobName} via [AutoScheduled] attribute with schedule {JobSchedule}", candidate.Name, scheduleAttr.CronExpression); - _recurringJobManager.AddOrUpdate(candidate.Name, new Job(candidate, candidate.GetMethod(scheduleAttr.MethodName)), scheduleAttr.CronExpression); - } + _logger.LogError(ex, "Unable to activate job {JobName}: {ExceptionMessage}", candidate.Name, ex.Message); + } + } + else + { + var scheduleAttr = candidate.GetCustomAttribute(); + if (scheduleAttr != null) + { + _logger.LogInformation("Auto-scheduling job {JobName} via [AutoScheduled] attribute with schedule {JobSchedule}", candidate.Name, scheduleAttr.CronExpression); + _recurringJobManager.AddOrUpdate(candidate.Name, new Job(candidate, candidate.GetMethod(scheduleAttr.MethodName)), scheduleAttr.CronExpression); } } } diff --git a/src/Webenable.Hangfire.Contrib/Internal/HangfireLogger.cs b/src/Webenable.Hangfire.Contrib/Internal/HangfireLogger.cs index 15d6241..c40c216 100644 --- a/src/Webenable.Hangfire.Contrib/Internal/HangfireLogger.cs +++ b/src/Webenable.Hangfire.Contrib/Internal/HangfireLogger.cs @@ -5,81 +5,80 @@ using Hangfire.Server; using Microsoft.Extensions.Logging; -namespace Webenable.Hangfire.Contrib.Internal +namespace Webenable.Hangfire.Contrib.Internal; + +public class HangfireLogger : ILogger { - public class HangfireLogger : ILogger - { - public IExternalScopeProvider? ScopeProvider { get; internal set; } + public IExternalScopeProvider? ScopeProvider { get; internal set; } - public IDisposable BeginScope(TState state) => ScopeProvider?.Push(state) ?? NoopDisposable.Instance; + public IDisposable BeginScope(TState state) => ScopeProvider?.Push(state) ?? NoopDisposable.Instance; - public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && ScopeProvider != null; + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && ScopeProvider != null; - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) { - if (!IsEnabled(logLevel)) - { - return; - } + return; + } + + try + { + PerformContext? ctx = null; + var msgBuilder = new StringBuilder(); - try + var scopeProvider = ScopeProvider; + scopeProvider?.ForEachScope((scopeValue, scopeState) => { - PerformContext? ctx = null; - var msgBuilder = new StringBuilder(); + string? msg = null; + if (scopeValue is IReadOnlyList> kvp) + { + msg = kvp.ToString(); + } + if (scopeValue is string str) + { + msg = str; + } + if (msg != null && !msg.StartsWith("Job")) + { + msgBuilder.Append(msgBuilder.Length == 0 ? "" : " => ").Append(msg); + return; + } - var scopeProvider = ScopeProvider; - scopeProvider?.ForEachScope((scopeValue, scopeState) => + if (scopeValue is PerformContext performContext) { - string? msg = null; - if (scopeValue is IReadOnlyList> kvp) - { - msg = kvp.ToString(); - } - if (scopeValue is string str) - { - msg = str; - } - if (msg != null && !msg.StartsWith("Job")) - { - msgBuilder.Append(msgBuilder.Length == 0 ? "" : " => ").Append(msg); - return; - } + ctx = performContext; + } + }, state); - if (scopeValue is PerformContext performContext) - { - ctx = performContext; - } - }, state); + if (ctx != null) + { + msgBuilder.Append(msgBuilder.Length == 0 ? "" : " => ").Append(state?.ToString()); - if (ctx != null) + var color = logLevel switch { - msgBuilder.Append(msgBuilder.Length == 0 ? "" : " => ").Append(state?.ToString()); + LogLevel.Critical or LogLevel.Error => ConsoleTextColor.Red, + LogLevel.Warning => ConsoleTextColor.Yellow, + _ => ConsoleTextColor.White, + }; - var color = logLevel switch - { - LogLevel.Critical or LogLevel.Error => ConsoleTextColor.Red, - LogLevel.Warning => ConsoleTextColor.Yellow, - _ => ConsoleTextColor.White, - }; - - ctx.WriteLine(color, msgBuilder.ToString()); - } - } - catch (Exception ex) - { - // Logging should never throw an exception - // Write the exceptions to the console for visiblity - Console.WriteLine($"Failed to write Hangfire log: {ex}"); + ctx.WriteLine(color, msgBuilder.ToString()); } } - - private class NoopDisposable : IDisposable + catch (Exception ex) { - public static NoopDisposable Instance = new(); + // Logging should never throw an exception + // Write the exceptions to the console for visiblity + Console.WriteLine($"Failed to write Hangfire log: {ex}"); + } + } - public void Dispose() - { - } + private class NoopDisposable : IDisposable + { + public static NoopDisposable Instance = new(); + + public void Dispose() + { } } } diff --git a/src/Webenable.Hangfire.Contrib/Internal/HangfireLoggerProvider.cs b/src/Webenable.Hangfire.Contrib/Internal/HangfireLoggerProvider.cs index 7299f79..3ec7907 100644 --- a/src/Webenable.Hangfire.Contrib/Internal/HangfireLoggerProvider.cs +++ b/src/Webenable.Hangfire.Contrib/Internal/HangfireLoggerProvider.cs @@ -1,21 +1,20 @@ using System.Collections.Concurrent; using Microsoft.Extensions.Logging; -namespace Webenable.Hangfire.Contrib.Internal -{ - public sealed class HangfireLoggerProvider : ILoggerProvider, ISupportExternalScope - { - private readonly ConcurrentDictionary _loggers = new(); +namespace Webenable.Hangfire.Contrib.Internal; - private IExternalScopeProvider? _scopeProvider; +public sealed class HangfireLoggerProvider : ILoggerProvider, ISupportExternalScope +{ + private readonly ConcurrentDictionary _loggers = new(); - public ILogger CreateLogger(string categoryName) => - _loggers.GetOrAdd(categoryName, new HangfireLogger { ScopeProvider = _scopeProvider }); + private IExternalScopeProvider? _scopeProvider; - public void Dispose() - { - } + public ILogger CreateLogger(string categoryName) => + _loggers.GetOrAdd(categoryName, new HangfireLogger { ScopeProvider = _scopeProvider }); - public void SetScopeProvider(IExternalScopeProvider scopeProvider) => _scopeProvider = scopeProvider; + public void Dispose() + { } + + public void SetScopeProvider(IExternalScopeProvider scopeProvider) => _scopeProvider = scopeProvider; } diff --git a/src/Webenable.Hangfire.Contrib/Internal/PerformContextWrapper.cs b/src/Webenable.Hangfire.Contrib/Internal/PerformContextWrapper.cs index f98f891..14d0dad 100644 --- a/src/Webenable.Hangfire.Contrib/Internal/PerformContextWrapper.cs +++ b/src/Webenable.Hangfire.Contrib/Internal/PerformContextWrapper.cs @@ -1,13 +1,12 @@ using Hangfire.Server; -namespace Webenable.Hangfire.Contrib.Internal +namespace Webenable.Hangfire.Contrib.Internal; + +internal class PerformContextWrapper : PerformContext { - internal class PerformContextWrapper : PerformContext + public PerformContextWrapper(PerformContext context) : base(context) { - public PerformContextWrapper(PerformContext context) : base(context) - { - } - - public override string ToString() => BackgroundJob.Job.Type.Name; } + + public override string ToString() => BackgroundJob.Job.Type.Name; } diff --git a/src/Webenable.Hangfire.Contrib/JobExpirationAttribute.cs b/src/Webenable.Hangfire.Contrib/JobExpirationAttribute.cs index cb38b5e..1c07c5b 100644 --- a/src/Webenable.Hangfire.Contrib/JobExpirationAttribute.cs +++ b/src/Webenable.Hangfire.Contrib/JobExpirationAttribute.cs @@ -3,31 +3,30 @@ using Hangfire.States; using Hangfire.Storage; -namespace Webenable.Hangfire.Contrib +namespace Webenable.Hangfire.Contrib; + +/// +/// Sets the expiration timeout of a job. +/// +public class JobExpirationAttribute : JobFilterAttribute, IApplyStateFilter { /// /// Sets the expiration timeout of a job. /// - public class JobExpirationAttribute : JobFilterAttribute, IApplyStateFilter + public JobExpirationAttribute() { - /// - /// Sets the expiration timeout of a job. - /// - public JobExpirationAttribute() - { - } + } - /// - /// Gets or sets the expiration timeout duration in days. - /// - public int Days { get; set; } + /// + /// Gets or sets the expiration timeout duration in days. + /// + public int Days { get; set; } - /// - public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction) => - context.JobExpirationTimeout = TimeSpan.FromDays(Days); + /// + public void OnStateApplied(ApplyStateContext context, IWriteOnlyTransaction transaction) => + context.JobExpirationTimeout = TimeSpan.FromDays(Days); - /// - public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction) => - context.JobExpirationTimeout = TimeSpan.FromDays(Days); - } + /// + public void OnStateUnapplied(ApplyStateContext context, IWriteOnlyTransaction transaction) => + context.JobExpirationTimeout = TimeSpan.FromDays(Days); } diff --git a/src/Webenable.Hangfire.Contrib/Webenable.Hangfire.Contrib.csproj b/src/Webenable.Hangfire.Contrib/Webenable.Hangfire.Contrib.csproj index 2fad9f1..ee7b04b 100644 --- a/src/Webenable.Hangfire.Contrib/Webenable.Hangfire.Contrib.csproj +++ b/src/Webenable.Hangfire.Contrib/Webenable.Hangfire.Contrib.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0 - 3.1.1 + net6.0;net7.0 + 4.0.0 latest Useful and opinionated set of ASP.NET Core integration extensions for Hangfire. true @@ -18,8 +18,8 @@ - - + + diff --git a/test/Webenable.Hangfire.Contrib.Tests/HangfireJobTests.cs b/test/Webenable.Hangfire.Contrib.Tests/HangfireJobTests.cs index 5a4e220..0139fdb 100644 --- a/test/Webenable.Hangfire.Contrib.Tests/HangfireJobTests.cs +++ b/test/Webenable.Hangfire.Contrib.Tests/HangfireJobTests.cs @@ -3,36 +3,35 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace Webenable.Hangfire.Contrib.Tests +namespace Webenable.Hangfire.Contrib.Tests; + +public class HangfireJobTests { - public class HangfireJobTests + [Fact] + public async Task ExecutesWithNullParams() { - [Fact] - public async Task ExecutesWithNullParams() - { - // Arrange - var job = new FooJob(new LoggerFactory()); + // Arrange + var job = new FooJob(new LoggerFactory()); - // Act - await job.ExecuteAsync(null, null); + // Act + await job.ExecuteAsync(null, null); - // Assert - Assert.True(job.Executed); - } + // Assert + Assert.True(job.Executed); } +} - public class FooJob : HangfireJob +public class FooJob : HangfireJob +{ + public FooJob(ILoggerFactory loggerFactory) : base(loggerFactory) { - public FooJob(ILoggerFactory loggerFactory) : base(loggerFactory) - { - } - - protected override Task ExecuteCoreAsync(IJobCancellationToken cancellationToken) - { - Executed = true; - return Task.CompletedTask; - } + } - internal bool Executed { get; set; } + protected override Task ExecuteCoreAsync(IJobCancellationToken cancellationToken) + { + Executed = true; + return Task.CompletedTask; } + + internal bool Executed { get; set; } } diff --git a/test/Webenable.Hangfire.Contrib.Tests/Webenable.Hangfire.Contrib.Tests.csproj b/test/Webenable.Hangfire.Contrib.Tests/Webenable.Hangfire.Contrib.Tests.csproj index dd1262b..de9cf0c 100644 --- a/test/Webenable.Hangfire.Contrib.Tests/Webenable.Hangfire.Contrib.Tests.csproj +++ b/test/Webenable.Hangfire.Contrib.Tests/Webenable.Hangfire.Contrib.Tests.csproj @@ -1,11 +1,14 @@  - net5.0 + net7.0 - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +