From 28d19c5bca6c2acbf6c3954a98faddb3a530d43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=BCrzd=C3=B6rfer?= Date: Mon, 28 Oct 2024 17:14:22 +0100 Subject: [PATCH 1/5] enhance hangfire, add configurators --- .../Extensions/EnumerableExtensions.cs | 11 ++ .../Plugins/IPluginConfigurator.cs | 4 + .../Services/TypeImplementationResolver.cs | 36 +++- .../Attributes/JobTimeZoneAttribute.cs | 13 ++ .../Attributes/JobTimeoutAttribute.cs | 41 +++++ .../Attributes/RecurringJobAttribute.cs | 6 +- .../Attributes/UniquePerQueueAttribute.cs | 49 ++---- .../Extensions/MonitoringApiExtensions.cs | 28 +++ .../HangFirePlugin.cs | 37 ++-- .../HangfireConfiguration.cs | 2 + .../HangfireExtensions.cs | 3 +- .../IHangfireConfigurator.cs | 12 ++ .../IJobManager.cs | 32 ++++ .../Job/AsyncHangfireJob.cs | 5 +- .../Job/AsyncJob.cs | 5 +- .../Job/HangfireJob.cs | 6 +- .../Job/IJobDetailsCollection.cs | 6 - .../Job/IJobRegister.cs | 15 -- .../Job/IJobRegisterBuilder.cs | 8 - .../Job/IParameterizedAsyncJob.cs | 2 +- .../Job/JobDetailCollection.cs | 91 ---------- .../Job/JobDetails.cs | 13 -- .../Job/JobRegistrationBuilder.cs | 24 --- .../Job/ParameterizedAsyncJob.cs | 6 +- .../PiBox.Plugins.Jobs.Hangfire/JobManager.cs | 160 ++++++++++++++++++ .../PiBox.Plugins.Jobs.Hangfire/JobOptions.cs | 6 +- .../Attributes/EnabledByFeatureFilterTests.cs | 22 +-- .../JobCleanupExpirationTimeAttributeTests.cs | 4 +- .../HangfireExtensionsTests.cs | 5 +- .../HangfirePluginTests.cs | 58 +------ .../JobTest.cs | 46 +---- ...TestJobAsync.cs => TestJobTimeoutAsync.cs} | 21 ++- .../DependencyInjectionExtensions.cs | 12 +- .../EntityFrameworkPlugin.cs | 16 +- .../DependencyInjectionExtensionsTests.cs | 3 + .../EntityFrameworkPluginTests.cs | 23 ++- .../PiBox.Example.Service.csproj | 1 + .../PiBox.Example.Service/TestConfigurator.cs | 21 +++ .../src/PiBox.Example.Service/appsettings.yml | 6 + 39 files changed, 509 insertions(+), 350 deletions(-) create mode 100644 PiBox.Hosting/Abstractions/src/PiBox.Hosting.Abstractions/Extensions/EnumerableExtensions.cs create mode 100644 PiBox.Hosting/Abstractions/src/PiBox.Hosting.Abstractions/Plugins/IPluginConfigurator.cs create mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/JobTimeZoneAttribute.cs create mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/JobTimeoutAttribute.cs create mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Extensions/MonitoringApiExtensions.cs create mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IHangfireConfigurator.cs create mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IJobManager.cs delete mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobDetailsCollection.cs delete mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobRegister.cs delete mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobRegisterBuilder.cs delete mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobDetailCollection.cs delete mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobDetails.cs delete mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobRegistrationBuilder.cs create mode 100644 PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobManager.cs rename PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/{TestJobAsync.cs => TestJobTimeoutAsync.cs} (56%) create mode 100644 example/src/PiBox.Example.Service/TestConfigurator.cs diff --git a/PiBox.Hosting/Abstractions/src/PiBox.Hosting.Abstractions/Extensions/EnumerableExtensions.cs b/PiBox.Hosting/Abstractions/src/PiBox.Hosting.Abstractions/Extensions/EnumerableExtensions.cs new file mode 100644 index 0000000..47b14cf --- /dev/null +++ b/PiBox.Hosting/Abstractions/src/PiBox.Hosting.Abstractions/Extensions/EnumerableExtensions.cs @@ -0,0 +1,11 @@ +namespace PiBox.Hosting.Abstractions.Extensions +{ + public static class EnumerableExtensions + { + public static void ForEach(this IEnumerable source, Action action) + { + foreach (var item in source) + action(item); + } + } +} diff --git a/PiBox.Hosting/Abstractions/src/PiBox.Hosting.Abstractions/Plugins/IPluginConfigurator.cs b/PiBox.Hosting/Abstractions/src/PiBox.Hosting.Abstractions/Plugins/IPluginConfigurator.cs new file mode 100644 index 0000000..fff7a3f --- /dev/null +++ b/PiBox.Hosting/Abstractions/src/PiBox.Hosting.Abstractions/Plugins/IPluginConfigurator.cs @@ -0,0 +1,4 @@ +namespace PiBox.Hosting.Abstractions.Plugins +{ + public interface IPluginConfigurator : IPluginActivateable; +} diff --git a/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs b/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs index 62fd994..7adfcc0 100644 --- a/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs +++ b/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs @@ -1,7 +1,9 @@ +using System.Collections; using System.Reflection; using Microsoft.Extensions.Configuration; using PiBox.Hosting.Abstractions.Attributes; using PiBox.Hosting.Abstractions.Extensions; +using PiBox.Hosting.Abstractions.Plugins; using PiBox.Hosting.Abstractions.Services; namespace PiBox.Hosting.WebHost.Services @@ -36,6 +38,36 @@ private object GetArgument(Type instanceType, Type type) if (type.HasAttribute()) return GetConfiguration(type, type.GetAttribute()!.Section); + var isList = type.GetInterfaces().Any(i => i.IsAssignableTo(typeof(IEnumerable))); + var innerType = isList ? type.GetElementType()! : type; + + if (innerType.IsInterface && innerType.IsAssignableTo(typeof(IPluginConfigurator))) + { + if (!isList) + throw new NotSupportedException( + $"For plugin configurators you must implement to take in an enumerable of the configurators. Configurator: {innerType.Name}"); + var args = _resolvedTypes.Select(x => x.Assembly).Distinct() + .SelectMany(x => x.GetTypes()) + .Where(x => x.IsClass && !x.IsAbstract && x.IsAssignableTo(innerType)) + .Select(ResolveInstance) + .ToList(); + if (!isList) + return args.FirstOrDefault(); + + if (type.IsArray) + { + var array = Array.CreateInstance(innerType, args.Count); + args.ToArray().CopyTo(array, 0); + return array; + } + + var listType = typeof(List<>).MakeGenericType(innerType); + var list = (IList)Activator.CreateInstance(listType)!; + foreach (var item in args) + list.Add(item); + return list; + } + if (!(type.IsGenericType && _defaultArguments.ContainsKey(type.GetGenericTypeDefinition()))) { return null; @@ -69,9 +101,9 @@ public object ResolveInstance(Type type) if (type.HasAttribute()) return GetConfiguration(type, type.GetAttribute()!.Section); var constructor = type.GetConstructors().FirstOrDefault(); - var parameters = constructor?.GetParameters() ?? Array.Empty(); + var parameters = constructor?.GetParameters() ?? []; if (constructor is null || parameters.Length == 0) - return TrackInstance(Activator.CreateInstance(type, Array.Empty())); + return TrackInstance(Activator.CreateInstance(type, [])); var arguments = parameters.Select(parameter => GetArgument(type, parameter.ParameterType)).ToArray(); return TrackInstance(constructor.Invoke(arguments)); } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/JobTimeZoneAttribute.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/JobTimeZoneAttribute.cs new file mode 100644 index 0000000..2972164 --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/JobTimeZoneAttribute.cs @@ -0,0 +1,13 @@ +namespace PiBox.Plugins.Jobs.Hangfire.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class JobTimeZoneAttribute : Attribute + { + public JobTimeZoneAttribute(TimeZoneInfo timeZoneInfo) + { + TimeZoneInfo = timeZoneInfo; + } + + public TimeZoneInfo TimeZoneInfo { get; } + } +} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/JobTimeoutAttribute.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/JobTimeoutAttribute.cs new file mode 100644 index 0000000..b94b4d4 --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/JobTimeoutAttribute.cs @@ -0,0 +1,41 @@ +namespace PiBox.Plugins.Jobs.Hangfire.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class JobTimeoutAttribute : Attribute + { + public JobTimeoutAttribute(int timeout, TimeUnit unit) + { + switch (unit) + { + case TimeUnit.Milliseconds: + Timeout = TimeSpan.FromMilliseconds(timeout); + break; + case TimeUnit.Seconds: + Timeout = TimeSpan.FromSeconds(timeout); + break; + case TimeUnit.Minutes: + Timeout = TimeSpan.FromMinutes(timeout); + break; + case TimeUnit.Hours: + Timeout = TimeSpan.FromHours(timeout); + break; + case TimeUnit.Days: + Timeout = TimeSpan.FromDays(timeout); + break; + default: + throw new ArgumentOutOfRangeException(nameof(unit), unit, null); + } + } + + public TimeSpan Timeout { get; } + } + + public enum TimeUnit + { + Milliseconds, + Seconds, + Minutes, + Hours, + Days + } +} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/RecurringJobAttribute.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/RecurringJobAttribute.cs index 76abbba..ab5cff6 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/RecurringJobAttribute.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/RecurringJobAttribute.cs @@ -1,13 +1,17 @@ +using Hangfire.States; + namespace PiBox.Plugins.Jobs.Hangfire.Attributes { [AttributeUsage(AttributeTargets.Class)] public class RecurringJobAttribute : Attribute { - public RecurringJobAttribute(string cronPattern) + public RecurringJobAttribute(string cronPattern, string queue = EnqueuedState.DefaultQueue) { CronPattern = cronPattern; + Queue = queue; } public string CronPattern { get; } + public string Queue { get; } } } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/UniquePerQueueAttribute.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/UniquePerQueueAttribute.cs index 28bdf36..56041bc 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/UniquePerQueueAttribute.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Attributes/UniquePerQueueAttribute.cs @@ -1,14 +1,13 @@ using System.Text.Json; -using Hangfire; using Hangfire.Common; using Hangfire.States; -using Hangfire.Storage; -using Hangfire.Storage.Monitoring; +using PiBox.Plugins.Jobs.Hangfire.Extensions; namespace PiBox.Plugins.Jobs.Hangfire.Attributes { public class UniquePerQueueAttribute : JobFilterAttribute, IElectStateFilter { + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() { IncludeFields = false }; public string Queue { get; set; } public bool CheckScheduledJobs { get; set; } @@ -23,66 +22,42 @@ public UniquePerQueueAttribute(string queue) private IEnumerable GetJobs(ElectStateContext context) { - IMonitoringApi monitoringApi = context.Storage.GetMonitoringApi(); - List jobs = - new List(); - foreach ((string key, EnqueuedJobDto enqueuedJobDto1) in monitoringApi.EnqueuedJobs(Queue, 0, 500)) - { - string id = key; - EnqueuedJobDto enqueuedJobDto2 = enqueuedJobDto1; - jobs.Add(JobEntity.Parse(id, enqueuedJobDto2.Job)); - } + var monitoringApi = context.Storage.GetMonitoringApi(); + var jobs = new List(); + foreach (var (key, enqueuedJobDto1) in monitoringApi.GetCompleteList((api, page) => api.EnqueuedJobs(Queue, page.Offset, page.PageSize))) + jobs.Add(JobEntity.Parse(key, enqueuedJobDto1.Job)); if (CheckScheduledJobs) - { - foreach (KeyValuePair pair in monitoringApi.ScheduledJobs(0, 500)) - { - string id = pair.Key; - ScheduledJobDto scheduledJobDto3 = pair.Value; + foreach (var (id, scheduledJobDto3) in monitoringApi.GetCompleteList((api, page) => api.ScheduledJobs(page.Offset, page.PageSize))) jobs.Add(JobEntity.Parse(id, scheduledJobDto3.Job)); - } - } if (!CheckRunningJobs) - { return jobs; - } - foreach (KeyValuePair pair in - monitoringApi.ProcessingJobs(0, 500)) - { - string id = pair.Key; - ProcessingJobDto processingJobDto3 = pair.Value; + foreach (var (id, processingJobDto3) in monitoringApi.GetCompleteList((api, page) => api.ProcessingJobs(page.Offset, page.PageSize))) jobs.Add(JobEntity.Parse(id, processingJobDto3.Job)); - } - return jobs; } public void OnStateElection(ElectStateContext context) { - if (!(context.CandidateState is EnqueuedState candidateState)) - { + if (context.CandidateState is not EnqueuedState candidateState) return; - } candidateState.Queue = Queue; - BackgroundJob job = context.BackgroundJob; + var job = context.BackgroundJob; var filteredArguments = job.Job.Args.Where(x => x.GetType() != typeof(CancellationToken)).ToList(); - var jobArgs = JsonSerializer.Serialize(filteredArguments, - new JsonSerializerOptions() { IncludeFields = false }); + var jobArgs = JsonSerializer.Serialize(filteredArguments, _jsonSerializerOptions); var jobs = GetJobs(context); var jobsWithArgs = jobs .Select(x => new { JobEntity = x, ArgAsString = jobArgs }).ToList(); var alreadyExists = jobsWithArgs.Exists(x => x.JobEntity.Value.Method == job.Job.Method && x.ArgAsString == jobArgs && x.JobEntity.Id != job.Id); if (!alreadyExists) - { return; - } context.CandidateState = - new DeletedState() { Reason = "Instance of the same job is already queued." }; + new DeletedState { Reason = "Instance of the same job is already queued." }; } private sealed record JobEntity(string Id, global::Hangfire.Common.Job Value) diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Extensions/MonitoringApiExtensions.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Extensions/MonitoringApiExtensions.cs new file mode 100644 index 0000000..710b246 --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Extensions/MonitoringApiExtensions.cs @@ -0,0 +1,28 @@ +using Hangfire.Storage; + +namespace PiBox.Plugins.Jobs.Hangfire.Extensions +{ + public static class MonitoringApiExtensions + { + private const int PageSize = 500; + public static IList GetCompleteList(this IMonitoringApi api, Func> action) + { + var pageOpts = new HangfirePageOptions(0, PageSize); + var result = new List(); + while (true) + { + var partialResult = action(api, pageOpts).ToList(); + result.AddRange(partialResult); + if (partialResult.Count < pageOpts.PageSize) + break; + pageOpts = pageOpts.Next(); + } + return result; + } + } + + public record HangfirePageOptions(int Offset, int PageSize) + { + public HangfirePageOptions Next() => this with { Offset = Offset + PageSize }; + } +} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangFirePlugin.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangFirePlugin.cs index 556c829..52f1d08 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangFirePlugin.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangFirePlugin.cs @@ -2,6 +2,8 @@ using Hangfire.Dashboard; using Hangfire.MemoryStorage; using Hangfire.PostgreSql; +using Hangfire.States; +using Hangfire.Storage; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -13,6 +15,7 @@ using PiBox.Hosting.Abstractions.Services; using PiBox.Plugins.Jobs.Hangfire.Attributes; using PiBox.Plugins.Jobs.Hangfire.Job; +using BindingFlags = System.Reflection.BindingFlags; namespace PiBox.Plugins.Jobs.Hangfire { @@ -20,42 +23,49 @@ public class HangFirePlugin : IPluginServiceConfiguration, IPluginApplicationCon IPluginHealthChecksConfiguration { private readonly IImplementationResolver _implementationResolver; + private readonly IHangfireConfigurator[] _configurators; private readonly HangfireConfiguration _hangfireConfig; - public HangFirePlugin(HangfireConfiguration configuration, IImplementationResolver implementationResolver) + public HangFirePlugin(HangfireConfiguration configuration, IImplementationResolver implementationResolver, IHangfireConfigurator[] configurators) { _implementationResolver = implementationResolver; + _configurators = configurators; _hangfireConfig = configuration; } public void ConfigureServices(IServiceCollection serviceCollection) { serviceCollection.AddFeatureManagement(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(sp => sp.GetRequiredService()); serviceCollection.AddHangfire(conf => { conf.UseSerializerSettings(new JsonSerializerSettings()); if (_hangfireConfig.InMemory) - { conf.UseMemoryStorage(); - } else - { conf.UsePostgreSqlStorage(opts => opts.UseNpgsqlConnection(_hangfireConfig.ConnectionString)); - } - conf.UseSimpleAssemblyNameTypeSerializer(); + _configurators.ForEach(x => x.Configure(conf)); } ); serviceCollection.AddHangfireServer(options => { + options.Queues = _hangfireConfig.Queues.Union([EnqueuedState.DefaultQueue]).Distinct().ToArray(); if (_hangfireConfig.PollingIntervalInMs.HasValue) options.SchedulePollingInterval = TimeSpan.FromMilliseconds(_hangfireConfig.PollingIntervalInMs.Value); if (_hangfireConfig.WorkerCount.HasValue) options.WorkerCount = _hangfireConfig.WorkerCount.Value; + _configurators.ForEach(x => x.ConfigureServer(options)); + }); + serviceCollection.AddSingleton(sp => + { + var jobStorage = sp.GetRequiredService(); + var recurringJobManager = sp.GetRequiredService(); + var backgroundJobClient = sp.GetRequiredService(); + var hasQueueSupport = jobStorage.HasFeature(JobStorageFeatures.JobQueueProperty); + return new JobManager(hasQueueSupport, jobStorage.GetConnection(), jobStorage.GetMonitoringApi(), + recurringJobManager, backgroundJobClient); }); serviceCollection.AddHostedService(); } @@ -76,25 +86,24 @@ public void ConfigureApplication(IApplicationBuilder applicationBuilder) { Authorization = new List { urlAuthFilter } }); - var jobRegister = applicationBuilder.ApplicationServices.GetRequiredService(); + var jobRegister = applicationBuilder.ApplicationServices.GetRequiredService(); var jobOptions = applicationBuilder.ApplicationServices.GetService(); jobOptions?.ConfigureJobs.Invoke(jobRegister, applicationBuilder.ApplicationServices); - var registerJobMethod = jobRegister.GetType().GetMethod(nameof(IJobRegister.RegisterRecurringAsyncJob))!; + var registerJobMethod = jobRegister.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Single(x => x.Name == nameof(IJobManager.RegisterRecurring) && x.GetGenericArguments().Length == 1); foreach (var job in _implementationResolver.FindTypes(f => f.HasAttribute() && f.Implements())) { var recurringJobDetails = job.GetAttribute()!; var genericMethod = registerJobMethod.MakeGenericMethod(job); - genericMethod.Invoke(jobRegister, new object[] { recurringJobDetails.CronPattern }); + genericMethod.Invoke(jobRegister, [recurringJobDetails.CronPattern, recurringJobDetails.Queue]); } - - jobRegister.ActivateJobs(); } public void ConfigureHealthChecks(IHealthChecksBuilder healthChecksBuilder) { healthChecksBuilder.AddHangfire(s => s.MinimumAvailableServers = 1, "hangfire", - tags: new[] { HealthCheckTag.Readiness.Value }); + tags: [HealthCheckTag.Readiness.Value]); } internal class HostAuthorizationFilter : IDashboardAuthorizationFilter diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangfireConfiguration.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangfireConfiguration.cs index 84687d7..eb32054 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangfireConfiguration.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangfireConfiguration.cs @@ -1,3 +1,4 @@ +using Hangfire.States; using PiBox.Hosting.Abstractions.Attributes; namespace PiBox.Plugins.Jobs.Hangfire @@ -15,6 +16,7 @@ public class HangfireConfiguration public bool EnableJobsByFeatureManagementConfig { get; set; } public int? PollingIntervalInMs { get; set; } public int? WorkerCount { get; set; } + public string[] Queues { get; set; } = [EnqueuedState.DefaultQueue]; public string ConnectionString => $"Host={Host};Port={Port};Database={Database};Username={User};Password={Password};"; } } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangfireExtensions.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangfireExtensions.cs index 654bdce..4fe5442 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangfireExtensions.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangfireExtensions.cs @@ -1,11 +1,10 @@ using Microsoft.Extensions.DependencyInjection; -using PiBox.Plugins.Jobs.Hangfire.Job; namespace PiBox.Plugins.Jobs.Hangfire { public static class HangfireExtensions { - public static void ConfigureJobs(this IServiceCollection serviceCollection, Action configure) + public static void ConfigureJobs(this IServiceCollection serviceCollection, Action configure) { serviceCollection.AddSingleton(new JobOptions(configure)); } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IHangfireConfigurator.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IHangfireConfigurator.cs new file mode 100644 index 0000000..d6e54b2 --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IHangfireConfigurator.cs @@ -0,0 +1,12 @@ +using Hangfire; +using PiBox.Hosting.Abstractions.Plugins; + +namespace PiBox.Plugins.Jobs.Hangfire +{ + public interface IHangfireConfigurator : IPluginConfigurator + { + public bool IncludesStorage { get; } + void Configure(IGlobalConfiguration config); + void ConfigureServer(BackgroundJobServerOptions options); + } +} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IJobManager.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IJobManager.cs new file mode 100644 index 0000000..15412e9 --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IJobManager.cs @@ -0,0 +1,32 @@ +using Hangfire.States; +using Hangfire.Storage; +using Hangfire.Storage.Monitoring; +using PiBox.Plugins.Jobs.Hangfire.Job; + +namespace PiBox.Plugins.Jobs.Hangfire +{ + public interface IJobManager + { + IStorageConnection Connection { get; } + IMonitoringApi MonitoringApi { get; } + IList GetRecurringJobs(); + IList GetQueues(); + IList GetEnqueuedJobs(); + IList GetProcessingJobs(); + IList GetFailedJobs(); + IList GetFetchedJobs(); + IList GetRecurringJobs(Predicate predicate = null); + IList GetFetchedJobs(Predicate predicate = null); + IList GetEnqueuedJobs(Predicate predicate = null); + IList GetProcessingJobs(Predicate predicate = null); + IList GetFailedJobs(Predicate predicate = null); + string Enqueue(TJobParams parameters, string queue = EnqueuedState.DefaultQueue) where TJob : IParameterizedAsyncJob; + string Enqueue(string queue = EnqueuedState.DefaultQueue) where TJob : IAsyncJob; + string Schedule(TimeSpan schedule, string queue = EnqueuedState.DefaultQueue) where TJob : IAsyncJob; + string Schedule(TJobParams parameters, TimeSpan schedule, string queue = EnqueuedState.DefaultQueue) where TJob : IParameterizedAsyncJob; + void RegisterRecurring(string cron, string queue = EnqueuedState.DefaultQueue) where TJob : IAsyncJob; + void RegisterRecurring(string cron, TJobParams parameters, string queue = EnqueuedState.DefaultQueue) where TJob : IParameterizedAsyncJob; + void DeleteRecurring(string id); + void Delete(string id); + } +} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncHangfireJob.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncHangfireJob.cs index 592ea65..1db1451 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncHangfireJob.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncHangfireJob.cs @@ -10,18 +10,15 @@ protected AsyncHangfireJob(ILogger logger) : base(logger) protected async Task InternalExecuteAsync(Func> action) { - object result; try { - result = await action().ConfigureAwait(false); + return await action().ConfigureAwait(false); } catch (Exception exception) { Logger.LogError(exception, "Job failed"); throw; } - - return result; } } } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncJob.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncJob.cs index aafdb21..ca79225 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncJob.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncJob.cs @@ -12,15 +12,14 @@ public Task ExecuteAsync(CancellationToken jobCancellationToken) { return InternalExecuteAsync(async () => { - var timeout = JobOptionsCollection?.FirstOrDefault(x => x.JobType == GetType())?.Timeout; - if (timeout == null) + if (Timeout == null) { return await ExecuteJobAsync(jobCancellationToken); } using var cts = CancellationTokenSource.CreateLinkedTokenSource(jobCancellationToken); - cts.CancelAfter(timeout.Value); + cts.CancelAfter(Timeout.Value); return await ExecuteJobAsync(cts.Token); }); } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/HangfireJob.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/HangfireJob.cs index 36e6155..5335773 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/HangfireJob.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/HangfireJob.cs @@ -1,16 +1,18 @@ using Microsoft.Extensions.Logging; +using PiBox.Hosting.Abstractions.Extensions; +using PiBox.Plugins.Jobs.Hangfire.Attributes; namespace PiBox.Plugins.Jobs.Hangfire.Job { public abstract class HangfireJob : IDisposable { protected readonly ILogger Logger; - - public IJobDetailsCollection JobOptionsCollection { get; set; } + protected readonly TimeSpan? Timeout; protected HangfireJob(ILogger logger) { Logger = logger; + Timeout = GetType().GetAttribute()?.Timeout; } public void Dispose() diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobDetailsCollection.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobDetailsCollection.cs deleted file mode 100644 index bebb98d..0000000 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobDetailsCollection.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PiBox.Plugins.Jobs.Hangfire.Job -{ - public interface IJobDetailsCollection : IList, IJobRegister - { - } -} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobRegister.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobRegister.cs deleted file mode 100644 index aab66f0..0000000 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobRegister.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace PiBox.Plugins.Jobs.Hangfire.Job -{ - public interface IJobRegister - { - TimeZoneInfo DefaultTimeZoneInfo { get; set; } - public TimeSpan? DefaultTimeout { get; set; } - - IJobRegisterBuilder RegisterRecurringAsyncJob(string cronExpression) where T : IAsyncJob; - - IJobRegisterBuilder RegisterParameterizedRecurringAsyncJob(string cronExpression, TJobParams parameters, - string jobSuffix = "") where TJob : IParameterizedAsyncJob; - - void ActivateJobs(); - } -} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobRegisterBuilder.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobRegisterBuilder.cs deleted file mode 100644 index a9d8c71..0000000 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IJobRegisterBuilder.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace PiBox.Plugins.Jobs.Hangfire.Job -{ - public interface IJobRegisterBuilder - { - IJobRegisterBuilder UseTimezone(TimeZoneInfo timeZoneInfo); - IJobRegisterBuilder UseTimeout(TimeSpan timeout); - } -} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IParameterizedAsyncJob.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IParameterizedAsyncJob.cs index 12f55ad..9af0b53 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IParameterizedAsyncJob.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IParameterizedAsyncJob.cs @@ -2,6 +2,6 @@ namespace PiBox.Plugins.Jobs.Hangfire.Job { public interface IParameterizedAsyncJob : IDisposable { - Task ExecuteAsync(T value, CancellationToken jobCancellationToken); + Task ExecuteAsync(T value, CancellationToken jobCancellationToken = default); } } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobDetailCollection.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobDetailCollection.cs deleted file mode 100644 index 60a7c53..0000000 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobDetailCollection.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Hangfire; - -namespace PiBox.Plugins.Jobs.Hangfire.Job -{ - public class JobDetailCollection : List, IJobDetailsCollection - { - public TimeZoneInfo DefaultTimeZoneInfo { get; set; } = TimeZoneInfo.Utc; - public TimeSpan? DefaultTimeout { get; set; } - - public IJobRegisterBuilder RegisterRecurringAsyncJob(string cronExpression) where T : IAsyncJob - { - var jobId = GetJobName(); - - var options = new JobDetails(); - options.Name = jobId; - options.Timeout = DefaultTimeout; - options.TimeZoneInfo = DefaultTimeZoneInfo; - options.CronExpression = cronExpression; - options.JobType = typeof(T); - options.JobRegistration = () => - RecurringJob.AddOrUpdate(jobId, x => x.ExecuteAsync(CancellationToken.None), - options.CronExpression, new RecurringJobOptions { TimeZone = options.TimeZoneInfo }); - Add(options); - return new JobRegistrationBuilder(options); - } - - public IJobRegisterBuilder RegisterParameterizedRecurringAsyncJob(string cronExpression, TJobParams parameters, - string jobSuffix = "") where TJob : IParameterizedAsyncJob - { - var jobId = GetJobName(parameters, jobSuffix); - - var options = new JobDetails(); - options.Name = jobId; - options.Timeout = DefaultTimeout; - options.TimeZoneInfo = DefaultTimeZoneInfo; - options.CronExpression = cronExpression; - options.JobType = typeof(TJob); - options.JobParameter = parameters; - options.JobRegistration = () => RecurringJob.AddOrUpdate(jobId, - x => x.ExecuteAsync(parameters, CancellationToken.None), options.CronExpression, - new RecurringJobOptions { TimeZone = options.TimeZoneInfo }); - Add(options); - return new JobRegistrationBuilder(options); - } - - public void ActivateJobs() - { - foreach (var jobOptions in this.Where(x => x.JobRegistration != null)) - { - jobOptions.JobRegistration!(); - } - } - - private static string GetJobName(string jobIdSuffix = "") - { - var name = typeof(T).Name; - if (name.EndsWith("Job", StringComparison.OrdinalIgnoreCase)) - { - name = name.Substring(0, name.Length - 3); - } - - if (name.EndsWith("Async", StringComparison.OrdinalIgnoreCase)) - { - name = name.Substring(0, name.Length - 5); - } - - if (!string.IsNullOrEmpty(jobIdSuffix)) - { - name += $"_{jobIdSuffix}"; - } - - return name; - } - - private static string GetJobName(TJobParam jobParam, string jobIdSuffix = "") - { - var suffix = string.Empty; - if (!string.IsNullOrEmpty(jobIdSuffix)) - - { - suffix = jobIdSuffix; - } - else if (jobParam != null) - { - suffix = jobParam.ToString(); - } - - return GetJobName(suffix!); - } - } -} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobDetails.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobDetails.cs deleted file mode 100644 index a2392b6..0000000 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobDetails.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace PiBox.Plugins.Jobs.Hangfire.Job -{ - public class JobDetails - { - public Type JobType { get; set; } - public string Name { get; set; } - public string CronExpression { get; set; } - public TimeSpan? Timeout { get; set; } - internal Action JobRegistration { get; set; } - public TimeZoneInfo TimeZoneInfo { get; set; } - public object JobParameter { get; set; } - } -} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobRegistrationBuilder.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobRegistrationBuilder.cs deleted file mode 100644 index 3c7dbe9..0000000 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/JobRegistrationBuilder.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace PiBox.Plugins.Jobs.Hangfire.Job -{ - public class JobRegistrationBuilder : IJobRegisterBuilder - { - private readonly JobDetails _jobDetails; - - public JobRegistrationBuilder(JobDetails details) - { - _jobDetails = details; - } - - public IJobRegisterBuilder UseTimezone(TimeZoneInfo timeZoneInfo) - { - _jobDetails.TimeZoneInfo = timeZoneInfo; - return this; - } - - public IJobRegisterBuilder UseTimeout(TimeSpan timeout) - { - _jobDetails.Timeout = timeout; - return this; - } - } -} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/ParameterizedAsyncJob.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/ParameterizedAsyncJob.cs index 469c370..e873ab8 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/ParameterizedAsyncJob.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/ParameterizedAsyncJob.cs @@ -12,16 +12,14 @@ public Task ExecuteAsync(T value, CancellationToken jobCancellationToken { return InternalExecuteAsync(async () => { - var timeout = JobOptionsCollection - ?.FirstOrDefault(x => x.JobType == GetType() && Equals(x.JobParameter, value))?.Timeout; - if (timeout == null) + if (Timeout == null) { return await ExecuteJobAsync(value, jobCancellationToken); } using var cts = CancellationTokenSource.CreateLinkedTokenSource(jobCancellationToken); - cts.CancelAfter(timeout.Value); + cts.CancelAfter(Timeout.Value); return await ExecuteJobAsync(value, cts.Token); }); } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobManager.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobManager.cs new file mode 100644 index 0000000..6a652ed --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobManager.cs @@ -0,0 +1,160 @@ +using Hangfire; +using Hangfire.States; +using Hangfire.Storage; +using Hangfire.Storage.Monitoring; +using PiBox.Hosting.Abstractions.Extensions; +using PiBox.Plugins.Jobs.Hangfire.Attributes; +using PiBox.Plugins.Jobs.Hangfire.Extensions; +using PiBox.Plugins.Jobs.Hangfire.Job; + +namespace PiBox.Plugins.Jobs.Hangfire +{ + internal class JobManager : IJobManager + { + private record PageOptions(int Offset, int PageSize) + { + public PageOptions Next() => this with { Offset = Offset + PageSize }; + } + + private readonly IRecurringJobManager _recurringJobManager; + private readonly IBackgroundJobClient _backgroundJobClient; + private readonly bool _hasQueueSupport; + public IStorageConnection Connection { get; } + public IMonitoringApi MonitoringApi { get; } + + public JobManager(bool hasQueueSupport, + IStorageConnection connection, + IMonitoringApi monitoringApi, + IRecurringJobManager recurringJobManager, + IBackgroundJobClient backgroundJobClient) + { + _recurringJobManager = recurringJobManager; + _backgroundJobClient = backgroundJobClient; + Connection = connection; + MonitoringApi = monitoringApi; + _hasQueueSupport = hasQueueSupport; + } + + private static string GetJobName(string jobIdSuffix = "") + { + var name = typeof(T).Name; + if (name.EndsWith("Job", StringComparison.OrdinalIgnoreCase)) + name = name[..^3]; + if (name.EndsWith("Async", StringComparison.OrdinalIgnoreCase)) + name = name[..^5]; + if (!string.IsNullOrEmpty(jobIdSuffix)) + name += $"_{jobIdSuffix}"; + return name; + } + + public IList GetRecurringJobs() => + Connection.GetRecurringJobs(); + public IList GetQueues() => MonitoringApi.Queues().Select(x => x.Name).ToList(); + + public IList GetEnqueuedJobs() => + GetQueues() + .SelectMany(queue => MonitoringApi.GetCompleteList((api, page) => api.EnqueuedJobs(queue, page.Offset, page.PageSize))) + .Select(x => x.Value) + .ToList(); + + public IList GetProcessingJobs() => + GetQueues() + .SelectMany(queue => MonitoringApi.GetCompleteList((api, page) => api.ProcessingJobs(page.Offset, page.PageSize))) + .Select(x => x.Value) + .ToList(); + + public IList GetFailedJobs() => + MonitoringApi.GetCompleteList((api, page) => api.FailedJobs(page.Offset, page.PageSize)) + .Select(x => x.Value) + .ToList(); + + public IList GetFetchedJobs() => + GetQueues() + .SelectMany(queue => MonitoringApi.GetCompleteList((api, page) => api.FetchedJobs(queue, page.Offset, page.PageSize))) + .Select(x => x.Value) + .ToList(); + + public IList GetJobs() + { + var jobs = GetEnqueuedJobs().Select(x => x.Job).ToList(); + jobs.AddRange(GetProcessingJobs().Select(x => x.Job)); + jobs.AddRange(GetFailedJobs().Select(x => x.Job)); + jobs.AddRange(GetFetchedJobs().Select(x => x.Job)); + jobs.AddRange(GetRecurringJobs().Select(x => x.Job)); + return jobs; + } + + public IList GetRecurringJobs(Predicate predicate = null) => + GetRecurringJobs().Where(x => x.Job.Type == typeof(T) && (predicate == null || predicate(x))).ToList(); + + public IList GetFetchedJobs(Predicate predicate = null) => + GetFetchedJobs().Where(x => x.Job.Type == typeof(T) && (predicate == null || predicate(x))).ToList(); + + public IList GetEnqueuedJobs(Predicate predicate = null) => + GetEnqueuedJobs().Where(x => x.Job.Type == typeof(T) && (predicate == null || predicate(x))).ToList(); + + public IList GetProcessingJobs(Predicate predicate = null) => + GetProcessingJobs().Where(x => x.Job.Type == typeof(T) && (predicate == null || predicate(x))).ToList(); + + public IList GetFailedJobs(Predicate predicate = null) => + GetFailedJobs().Where(x => x.Job.Type == typeof(T) && (predicate == null || predicate(x))).ToList(); + + public string Enqueue(TJobParams parameters, string queue = EnqueuedState.DefaultQueue) + where TJob : IParameterizedAsyncJob + { + return _hasQueueSupport + ? _backgroundJobClient.Enqueue(queue, x => x.ExecuteAsync(parameters, CancellationToken.None)) + : _backgroundJobClient.Enqueue(x => x.ExecuteAsync(parameters, CancellationToken.None)); + } + + public string Enqueue(string queue = EnqueuedState.DefaultQueue) where TJob : IAsyncJob + { + return _hasQueueSupport + ? _backgroundJobClient.Enqueue(x => x.ExecuteAsync(CancellationToken.None)) + : _backgroundJobClient.Enqueue(queue, x => x.ExecuteAsync(CancellationToken.None)); + } + + public string Schedule(TimeSpan schedule, string queue = EnqueuedState.DefaultQueue) + where TJob : IAsyncJob + { + return _hasQueueSupport + ? _backgroundJobClient.Schedule(queue, x => x.ExecuteAsync(CancellationToken.None), schedule) + : _backgroundJobClient.Schedule(x => x.ExecuteAsync(CancellationToken.None), schedule); + } + + public string Schedule(TJobParams parameters, TimeSpan schedule, + string queue = EnqueuedState.DefaultQueue) where TJob : IParameterizedAsyncJob + { + return _hasQueueSupport + ? _backgroundJobClient.Schedule(queue, x => x.ExecuteAsync(parameters, CancellationToken.None), schedule) + : _backgroundJobClient.Schedule(x => x.ExecuteAsync(parameters, CancellationToken.None), schedule); + } + + public void RegisterRecurring(string cron, string queue = EnqueuedState.DefaultQueue) + where TJob : IAsyncJob + { + if (_hasQueueSupport) + _recurringJobManager.AddOrUpdate(GetJobName(), queue, x => x.ExecuteAsync(CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); + else + _recurringJobManager.AddOrUpdate(GetJobName(), x => x.ExecuteAsync(CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); + } + + public void RegisterRecurring(string cron, TJobParams parameters, + string queue = EnqueuedState.DefaultQueue) where TJob : IParameterizedAsyncJob + { + if (_hasQueueSupport) + _recurringJobManager.AddOrUpdate(GetJobName(), queue, x => x.ExecuteAsync(parameters, CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); + else + _recurringJobManager.AddOrUpdate(GetJobName(), x => x.ExecuteAsync(parameters, CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); + } + + public void DeleteRecurring(string id) => _recurringJobManager.RemoveIfExists(id); + + public void Delete(string id) => _backgroundJobClient.Delete(id); + + private static TimeZoneInfo GetTimeZone() + { + return typeof(TJob).GetAttribute()?.TimeZoneInfo ?? TimeZoneInfo.Utc; + } + } +} diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobOptions.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobOptions.cs index 8c8e540..a70f29a 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobOptions.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobOptions.cs @@ -1,10 +1,8 @@ -using PiBox.Plugins.Jobs.Hangfire.Job; - namespace PiBox.Plugins.Jobs.Hangfire { public class JobOptions { - public JobOptions(Action configureJobs) => ConfigureJobs = configureJobs; - public Action ConfigureJobs { get; } + public JobOptions(Action configureJobs) => ConfigureJobs = configureJobs; + public Action ConfigureJobs { get; } } } diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/EnabledByFeatureFilterTests.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/EnabledByFeatureFilterTests.cs index 936c234..da8a9ae 100644 --- a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/EnabledByFeatureFilterTests.cs +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/EnabledByFeatureFilterTests.cs @@ -20,12 +20,12 @@ public void JobIsNotCancelledWhenMatchingFeatureIsEnabled() { JobStorage.Current = new MemoryStorage(); var loggerFactory = Substitute.For(); - var fakeLogger = new FakeLogger(); + var fakeLogger = new FakeLogger(); loggerFactory.CreateLogger(default!).ReturnsForAnyArgs(fakeLogger); var filter = new LogJobExecutionFilter(loggerFactory); - var job = new global::Hangfire.Common.Job(typeof(TestJobAsync), - typeof(TestJobAsync).GetMethod(nameof(TestJobAsync.ExecuteAsync)), CancellationToken.None); + var job = new global::Hangfire.Common.Job(typeof(TestJobTimeoutAsync), + typeof(TestJobTimeoutAsync).GetMethod(nameof(TestJobTimeoutAsync.ExecuteAsync)), CancellationToken.None); var performContext = new PerformContext(JobStorage.Current, Substitute.For(), new BackgroundJob("id1", job, DateTime.Now), @@ -53,12 +53,12 @@ public void JobIsNotCancelledWhenMatchingFeatureIsEnabled() { JobStorage.Current = new MemoryStorage(); var featureManager = Substitute.For(); - featureManager.IsEnabledAsync(Arg.Is(x => x == nameof(TestJobAsync))).Returns(true); + featureManager.IsEnabledAsync(Arg.Is(x => x == nameof(TestJobTimeoutAsync))).Returns(true); var filter = new EnabledByFeatureFilter(featureManager, new FakeLogger()); - var job = new global::Hangfire.Common.Job(typeof(TestJobAsync), - typeof(TestJobAsync).GetMethod(nameof(TestJobAsync.ExecuteAsync)), CancellationToken.None); + var job = new global::Hangfire.Common.Job(typeof(TestJobTimeoutAsync), + typeof(TestJobTimeoutAsync).GetMethod(nameof(TestJobTimeoutAsync.ExecuteAsync)), CancellationToken.None); var context = new PerformingContext( new PerformContext(JobStorage.Current, Substitute.For(), @@ -78,12 +78,12 @@ public void JobIsCancelledWhenMatchingFeatureIsDisabled() { JobStorage.Current = new MemoryStorage(); var featureManager = Substitute.For(); - featureManager.IsEnabledAsync(Arg.Is(x => x == nameof(TestJobAsync))).Returns(false); + featureManager.IsEnabledAsync(Arg.Is(x => x == nameof(TestJobTimeoutAsync))).Returns(false); var filter = new EnabledByFeatureFilter(featureManager, new FakeLogger()); - var job = new global::Hangfire.Common.Job(typeof(TestJobAsync), - typeof(TestJobAsync).GetMethod(nameof(TestJobAsync.ExecuteAsync)), CancellationToken.None); + var job = new global::Hangfire.Common.Job(typeof(TestJobTimeoutAsync), + typeof(TestJobTimeoutAsync).GetMethod(nameof(TestJobTimeoutAsync.ExecuteAsync)), CancellationToken.None); var context = new PerformingContext( new PerformContext(JobStorage.Current, Substitute.For(), @@ -107,8 +107,8 @@ public void JobIsCancelledWhenThereIsNoMatchingFeature() var filter = new EnabledByFeatureFilter(featureManager, new FakeLogger()); - var job = new global::Hangfire.Common.Job(typeof(TestJobAsync), - typeof(TestJobAsync).GetMethod(nameof(TestJobAsync.ExecuteAsync)), CancellationToken.None); + var job = new global::Hangfire.Common.Job(typeof(TestJobTimeoutAsync), + typeof(TestJobTimeoutAsync).GetMethod(nameof(TestJobTimeoutAsync.ExecuteAsync)), CancellationToken.None); var performContext = new PerformContext(JobStorage.Current, Substitute.For(), new BackgroundJob("id1", job, DateTime.Now), diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobCleanupExpirationTimeAttributeTests.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobCleanupExpirationTimeAttributeTests.cs index dbb3440..9e9a0be 100644 --- a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobCleanupExpirationTimeAttributeTests.cs +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobCleanupExpirationTimeAttributeTests.cs @@ -18,8 +18,8 @@ public void JobExpirationTimeoutIsAppliedCorrectly() var filter = new JobCleanupExpirationTimeAttribute(9999); - var job = new global::Hangfire.Common.Job(typeof(TestJobAsync), - typeof(TestJobAsync).GetMethod(nameof(TestJobAsync.ExecuteAsync)), CancellationToken.None); + var job = new global::Hangfire.Common.Job(typeof(TestJobTimeoutAsync), + typeof(TestJobTimeoutAsync).GetMethod(nameof(TestJobTimeoutAsync.ExecuteAsync)), CancellationToken.None); var writeOnlyTransaction = Substitute.For(); var context = new ApplyStateContext( diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfireExtensionsTests.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfireExtensionsTests.cs index f9a2613..be57524 100644 --- a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfireExtensionsTests.cs +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfireExtensionsTests.cs @@ -2,7 +2,6 @@ using Hangfire; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; -using PiBox.Plugins.Jobs.Hangfire.Job; using PiBox.Testing; namespace PiBox.Plugins.Jobs.Hangfire.Tests @@ -13,8 +12,8 @@ public class HangfireExtensionsTests public void CanSetupJobsWithAServiceCollection() { var sc = TestingDefaults.ServiceCollection(); - Action setup = (register, _) => - register.RegisterRecurringAsyncJob(Cron.Daily()); + Action setup = (register, _) => + register.RegisterRecurring(Cron.Daily()); sc.ConfigureJobs(setup); var sp = sc.BuildServiceProvider(); var options = sp.GetRequiredService(); diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfirePluginTests.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfirePluginTests.cs index f1c3c12..c16edc9 100644 --- a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfirePluginTests.cs +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfirePluginTests.cs @@ -15,13 +15,12 @@ using PiBox.Hosting.Abstractions; using PiBox.Hosting.Abstractions.Services; using PiBox.Plugins.Jobs.Hangfire.Attributes; -using PiBox.Plugins.Jobs.Hangfire.Job; namespace PiBox.Plugins.Jobs.Hangfire.Tests { public class HangfirePluginTests { - internal static HangfireConfiguration HangfireConfiguration = new() + internal static readonly HangfireConfiguration HangfireConfiguration = new() { Database = "testDatabase", Host = "testHost", @@ -37,7 +36,7 @@ public class HangfirePluginTests private readonly IImplementationResolver _implementationResolver = Substitute.For(); - private HangFirePlugin GetPlugin() => new(HangfireConfiguration, _implementationResolver); + private HangFirePlugin GetPlugin() => new(HangfireConfiguration, _implementationResolver, []); [Test] public void ConfigureServiceTest() @@ -56,7 +55,7 @@ public void ConfigureServiceTest() public void ConfigureApplicationTest() { _implementationResolver.FindTypes() - .Returns(new List { typeof(TestJobAsync) }); + .Returns(new List { typeof(TestJobTimeoutAsync) }); JobStorage.Current = new MemoryStorage(); var sc = new ServiceCollection(); sc.AddSingleton(NullLoggerFactory.Instance); @@ -65,56 +64,11 @@ public void ConfigureApplicationTest() var featureManager = Substitute.For(); plugin.ConfigureServices(sc); - sc.AddSingleton(featureManager); + sc.AddSingleton(featureManager); var serviceProvider = sc.BuildServiceProvider(); - var applicationBuilder = new ApplicationBuilder(serviceProvider); // need a real application builder here because of UseRouting() - var jobRegister = serviceProvider.GetRequiredService(); - jobRegister.DefaultTimeout = null; - jobRegister.DefaultTimeZoneInfo = TimeZoneInfo.Utc; - jobRegister - .RegisterParameterizedRecurringAsyncJob(Cron.Daily(), "hans"); - jobRegister.RegisterRecurringAsyncJob(Cron.Monthly()).UseTimeout(TimeSpan.FromSeconds(10)) - .UseTimezone(TimeZoneInfo.Local); - - jobRegister - .RegisterParameterizedRecurringAsyncJob(Cron.Weekly(), "cats", - "meow"); + var applicationBuilder = new ApplicationBuilder(serviceProvider); plugin.ConfigureApplication(applicationBuilder); - var collection = serviceProvider.GetRequiredService(); - - collection[0].Should().BeOfType(); - collection[0].CronExpression.Should().Be(Cron.Daily()); - collection[0].Name.Should().Be("ParameterizedAsyncJobTest_hans"); - collection[0].Timeout.Should().BeNull(); - collection[0].TimeZoneInfo.Should().Be(TimeZoneInfo.Utc); - collection[0].JobParameter.Should().Be("hans"); - collection[0].JobType.Should().Be(typeof(ParameterizedAsyncJobTest)); - - collection[1].Should().BeOfType(); - collection[1].CronExpression.Should().Be(Cron.Monthly()); - collection[1].Name.Should().Be("JobFails"); - collection[1].Timeout.Should().Be(TimeSpan.FromSeconds(10)); - collection[1].TimeZoneInfo.Should().Be(TimeZoneInfo.Local); - collection[1].JobParameter.Should().Be(null); - collection[1].JobType.Should().Be(typeof(JobFailsJob)); - - collection[2].Should().BeOfType(); - collection[2].CronExpression.Should().Be(Cron.Weekly()); - collection[2].Name.Should().Be("ParameterizedAsyncJobTest_meow"); - collection[2].Timeout.Should().BeNull(); - collection[2].TimeZoneInfo.Should().Be(TimeZoneInfo.Utc); - collection[2].JobParameter.Should().Be("cats"); - collection[2].JobType.Should().Be(typeof(ParameterizedAsyncJobTest)); - - collection[3].Should().BeOfType(); - collection[3].CronExpression.Should().Be(Cron.Daily()); - collection[3].Name.Should().Be("TestJob"); - collection[3].Timeout.Should().BeNull(); - collection[3].TimeZoneInfo.Should().Be(TimeZoneInfo.Utc); - collection[3].JobParameter.Should().Be(null); - collection[3].JobType.Should().Be(typeof(TestJobAsync)); - GlobalJobFilters.Filters.Should().Contain(x => x.Instance.GetType() == typeof(EnabledByFeatureFilter)); GlobalJobFilters.Filters.Should().Contain(x => x.Instance.GetType() == typeof(LogJobExecutionFilter)); } @@ -123,7 +77,7 @@ public void ConfigureApplicationTest() public void HangfireConfigureHealthChecksWorks() { var hcBuilder = Substitute.For(); - var plugin = new HangFirePlugin(HangfireConfiguration, _implementationResolver); + var plugin = new HangFirePlugin(HangfireConfiguration, _implementationResolver, []); plugin.ConfigureHealthChecks(hcBuilder); hcBuilder.Add(Arg.Is(h => h.Name == "hangfire" && h.Tags.Contains(HealthCheckTag.Readiness.Value))) .Received(Quantity.Exactly(1)); diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/JobTest.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/JobTest.cs index 244c249..38f7333 100644 --- a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/JobTest.cs +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/JobTest.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using NSubstitute; using NUnit.Framework; -using PiBox.Plugins.Jobs.Hangfire.Job; namespace PiBox.Plugins.Jobs.Hangfire.Tests { @@ -13,7 +12,7 @@ public class JobTest [Test] public async Task AsyncJobTest() { - var job = new TestJobAsync(_logger); + var job = new TestJobSuccessAsync(_logger); var result = await job.ExecuteAsync(CancellationToken.None); result.Should().Be("test"); } @@ -21,14 +20,7 @@ public async Task AsyncJobTest() [Test] public void AsyncJobWillBeCancelledAfterTimeout() { - var job = new TestJobAsync(_logger); - job.JobOptionsCollection = new JobDetailCollection(); - job.JobOptionsCollection.Add(new JobDetails - { - JobType = typeof(TestJobAsync), - Name = "Test", - Timeout = TimeSpan.FromMilliseconds(50) - }); + var job = new TestJobTimeoutAsync(_logger); job.Invoking(async x => await x.ExecuteAsync(new CancellationToken(true))).Should() .ThrowAsync(); } @@ -55,42 +47,8 @@ public void ParameterizedAsyncJobWillBeCancelledAfterTimeout() { var param = "Test"; var job = new ParameterizedAsyncJobTest(_logger); - job.JobOptionsCollection = new JobDetailCollection(); - job.JobOptionsCollection.Add(new JobDetails - { - JobType = typeof(ParameterizedAsyncJobTest), - JobParameter = param, - Name = "Test", - Timeout = TimeSpan.FromMilliseconds(50) - }); job.Invoking(async x => await x.ExecuteAsync(param, new CancellationToken(true))).Should() .ThrowAsync(); } - - [Test] - public void JobOptionTest() - { - var name = "Test"; - var jobType = typeof(string); - var cronExpression = "expression"; - var timeout = new TimeSpan(); - var timeZoneInfo = TimeZoneInfo.Local; - object jobParameter = "param"; - - var options = new JobDetails(); - options.Name = name; - options.JobType = jobType; - options.CronExpression = cronExpression; - options.Timeout = timeout; - options.TimeZoneInfo = timeZoneInfo; - options.JobParameter = jobParameter; - - options.Name.Should().Be(name); - options.JobType.Should().Be(jobType); - options.CronExpression.Should().Be(cronExpression); - options.Timeout.Should().Be(timeout); - options.TimeZoneInfo.Should().Be(timeZoneInfo); - options.JobParameter.Should().Be(jobParameter); - } } } diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/TestJobAsync.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/TestJobTimeoutAsync.cs similarity index 56% rename from PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/TestJobAsync.cs rename to PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/TestJobTimeoutAsync.cs index 539062e..b3b8a5d 100644 --- a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/TestJobAsync.cs +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/TestJobTimeoutAsync.cs @@ -5,9 +5,10 @@ namespace PiBox.Plugins.Jobs.Hangfire.Tests { [RecurringJob("0 0 * * *")] - public class TestJobAsync : AsyncJob + [JobTimeout(50, TimeUnit.Milliseconds)] + public class TestJobTimeoutAsync : AsyncJob { - public TestJobAsync(ILogger logger) : base(logger) + public TestJobTimeoutAsync(ILogger logger) : base(logger) { } @@ -20,6 +21,22 @@ protected override async Task ExecuteJobAsync(CancellationToken cancella } } + [RecurringJob("0 0 * * *")] + public class TestJobSuccessAsync : AsyncJob + { + public TestJobSuccessAsync(ILogger logger) : base(logger) + { + } + + protected override async Task ExecuteJobAsync(CancellationToken cancellationToken) + { + const string result = "test"; + Logger.LogInformation("Run"); + await Task.Delay(1, cancellationToken); + return result; + } + } + public class JobFailsJob : AsyncJob { public JobFailsJob(ILogger logger) : base(logger) diff --git a/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/DependencyInjectionExtensions.cs b/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/DependencyInjectionExtensions.cs index b8f8bc7..88a8d00 100644 --- a/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/DependencyInjectionExtensions.cs +++ b/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/DependencyInjectionExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using PiBox.Hosting.Abstractions; namespace PiBox.Plugins.Persistence.EntityFramework @@ -29,14 +30,21 @@ public static IHealthChecksBuilder AddEfContext(this IHealthChecksBuil where TContext : DbContext { healthChecksBuilder.AddDbContextCheck(typeof(TContext).Name, - tags: new[] { HealthCheckTag.Readiness.Value }); + tags: [HealthCheckTag.Readiness.Value]); return healthChecksBuilder; } public static void MigrateEfContexts(this IApplicationBuilder applicationBuilder) { + var logger = applicationBuilder.ApplicationServices.GetRequiredService>(); applicationBuilder.ApplicationServices.GetServices().ToList() - .ForEach(dbContext => dbContext.Migrate()); + .ForEach(dbContext => + { + var dbContextName = dbContext.GetType().Name; + logger.LogDebug("Migrating '{DbContextName}'", dbContextName); + dbContext.Migrate(); + logger.LogDebug("Migrated '{DbContextName}'", dbContextName); + }); } } } diff --git a/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/EntityFrameworkPlugin.cs b/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/EntityFrameworkPlugin.cs index 6099456..c63af55 100644 --- a/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/EntityFrameworkPlugin.cs +++ b/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/EntityFrameworkPlugin.cs @@ -1,14 +1,16 @@ using System.Diagnostics; +using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Instrumentation.EntityFrameworkCore; using OpenTelemetry.Trace; using PiBox.Hosting.Abstractions.Plugins; +using PiBox.Hosting.Abstractions.Services; using PiBox.Plugins.Persistence.Abstractions; namespace PiBox.Plugins.Persistence.EntityFramework { - public class EntityFrameworkPlugin : IPluginServiceConfiguration, IPluginApplicationConfiguration + public class EntityFrameworkPlugin(IImplementationResolver implementationResolver) : IPluginServiceConfiguration, IPluginApplicationConfiguration, IPluginHealthChecksConfiguration { public void ConfigureServices(IServiceCollection serviceCollection) { @@ -32,5 +34,17 @@ public void ConfigureApplication(IApplicationBuilder applicationBuilder) { DiagnosticListener.AllListeners.Subscribe(new DiagnosticObserver()); } + + public void ConfigureHealthChecks(IHealthChecksBuilder healthChecksBuilder) + { + var dbContexts = implementationResolver.FindAssemblies().SelectMany(x => x.GetTypes()) + .Where(x => x.GetInterfaces().Any(i => i == typeof(IDbContext))) + .ToList(); + var registerHc = typeof(DependencyInjectionExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public) + .Single(x => x.GetParameters().Length == 1 && x.GetParameters()[0].ParameterType == typeof(IHealthChecksBuilder)); + + foreach (var dbContext in dbContexts) + registerHc.MakeGenericMethod(dbContext).Invoke(null, [healthChecksBuilder]); + } } } diff --git a/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/DependencyInjectionExtensionsTests.cs b/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/DependencyInjectionExtensionsTests.cs index 9ef7a29..ddc7b83 100644 --- a/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/DependencyInjectionExtensionsTests.cs +++ b/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/DependencyInjectionExtensionsTests.cs @@ -3,9 +3,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; using NSubstitute; using NUnit.Framework; using PiBox.Hosting.Abstractions; +using PiBox.Testing.Assertions; namespace PiBox.Plugins.Persistence.EntityFramework.Tests { @@ -56,6 +58,7 @@ public void CanMigrateAllContexts() var sp = Substitute.For(); var dbContext = Substitute.For(); sp.GetService(typeof(IEnumerable)).Returns(new List { dbContext }); + sp.GetService(typeof(ILogger)).Returns(new FakeLogger()); var appBuilder = Substitute.For(); appBuilder.ApplicationServices.Returns(sp); appBuilder.MigrateEfContexts(); diff --git a/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/EntityFrameworkPluginTests.cs b/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/EntityFrameworkPluginTests.cs index f7ace1e..2101717 100644 --- a/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/EntityFrameworkPluginTests.cs +++ b/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/EntityFrameworkPluginTests.cs @@ -6,9 +6,11 @@ using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; using NSubstitute; using NUnit.Framework; using OpenTelemetry.Instrumentation.EntityFrameworkCore; +using PiBox.Hosting.Abstractions.Services; using PiBox.Plugins.Persistence.Abstractions; using PiBox.Testing.Extensions; @@ -16,7 +18,15 @@ namespace PiBox.Plugins.Persistence.EntityFramework.Tests { public class EntityFrameworkPluginTests { - private readonly EntityFrameworkPlugin _plugin = new(); + private IImplementationResolver _implementationResolver; + private EntityFrameworkPlugin _plugin; + + [SetUp] + public void Up() + { + _implementationResolver = Substitute.For(); + _plugin = new EntityFrameworkPlugin(_implementationResolver); + } [Test] public void ConfigureServicesWorks() @@ -51,6 +61,15 @@ public void ConfigureApplicationWorks() subscriber.Should().BeOfType(); } + [Test] + public void ConfiguresHealthChecks() + { + _implementationResolver.FindAssemblies().Returns([typeof(EntityFrameworkPluginTests).Assembly]); + var healthCheckBuilder = Substitute.For(); + _plugin.ConfigureHealthChecks(healthCheckBuilder); + healthCheckBuilder.Received(1).Add(Arg.Is(h => h.Name == nameof(TestContext))); + } + [Test] public void EnrichEfCoreWithActivitySetsOptions() { @@ -59,7 +78,7 @@ public void EnrichEfCoreWithActivitySetsOptions() opts.EnrichWithIDbCommand.Should().NotBeNull(); using var activity = new Activity("unit-test"); var command = Substitute.For(); - opts.EnrichWithIDbCommand(activity, command); + opts.EnrichWithIDbCommand!(activity, command); var dbNameTag = activity.Tags.Single(x => x.Key == "db.name"); dbNameTag.Should().NotBeNull(); diff --git a/example/src/PiBox.Example.Service/PiBox.Example.Service.csproj b/example/src/PiBox.Example.Service/PiBox.Example.Service.csproj index 5805a05..358e41c 100644 --- a/example/src/PiBox.Example.Service/PiBox.Example.Service.csproj +++ b/example/src/PiBox.Example.Service/PiBox.Example.Service.csproj @@ -18,6 +18,7 @@ + diff --git a/example/src/PiBox.Example.Service/TestConfigurator.cs b/example/src/PiBox.Example.Service/TestConfigurator.cs new file mode 100644 index 0000000..67d47f5 --- /dev/null +++ b/example/src/PiBox.Example.Service/TestConfigurator.cs @@ -0,0 +1,21 @@ +using Hangfire; +using Hangfire.MemoryStorage; +using PiBox.Plugins.Jobs.Hangfire; + +namespace PiBox.Example.Service +{ + public class TestConfigurator : IHangfireConfigurator + { + public bool IncludesStorage => false; + + public void Configure(IGlobalConfiguration config) + { + config.UseMemoryStorage(); + } + + public void ConfigureServer(BackgroundJobServerOptions options) + { + options.WorkerCount = 1; + } + } +} diff --git a/example/src/PiBox.Example.Service/appsettings.yml b/example/src/PiBox.Example.Service/appsettings.yml index d42ca2a..7d9ab2e 100644 --- a/example/src/PiBox.Example.Service/appsettings.yml +++ b/example/src/PiBox.Example.Service/appsettings.yml @@ -6,3 +6,9 @@ openapi: enabled: false tokenUrl: https://keycloak.example/realms/myrealm/protocol/openid-connect/token authUrl: https://keycloak.example/realms/myrealm/protocol/openid-connect/auth + +hangfire: + inMemory: true + allowedDashboardHost: localhost + queues: + - test From 5b5da1a16c208b27c40d6b22b34e1ab07b2f8162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=BCrzd=C3=B6rfer?= Date: Tue, 29 Oct 2024 12:08:15 +0100 Subject: [PATCH 2/5] enhance hangfire usage, add tests --- .../HangFirePlugin.cs | 2 +- .../IJobManager.cs | 6 +- .../Job/AsyncJob.cs | 2 +- .../Job/IAsyncJob.cs | 2 +- .../Job/ParameterizedAsyncJob.cs | 2 +- .../PiBox.Plugins.Jobs.Hangfire/JobManager.cs | 63 ++-- .../Attributes/JobTimeZoneAttributeTests.cs | 19 ++ .../Attributes/JobTimeoutAttributeTests.cs | 34 ++ .../MonitoringApiExtensionsTests.cs | 46 +++ .../HangfirePluginTests.cs | 26 +- .../JobManagerTests.cs | 314 ++++++++++++++++++ .../PiBox.Example.Service/TestConfigurator.cs | 21 -- .../src/PiBox.Example.Service/TestHangfire.cs | 36 ++ 13 files changed, 518 insertions(+), 55 deletions(-) create mode 100644 PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobTimeZoneAttributeTests.cs create mode 100644 PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobTimeoutAttributeTests.cs create mode 100644 PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Extensions/MonitoringApiExtensionsTests.cs create mode 100644 PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/JobManagerTests.cs delete mode 100644 example/src/PiBox.Example.Service/TestConfigurator.cs create mode 100644 example/src/PiBox.Example.Service/TestHangfire.cs diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangFirePlugin.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangFirePlugin.cs index 52f1d08..c7c13cd 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangFirePlugin.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/HangFirePlugin.cs @@ -96,7 +96,7 @@ public void ConfigureApplication(IApplicationBuilder applicationBuilder) { var recurringJobDetails = job.GetAttribute()!; var genericMethod = registerJobMethod.MakeGenericMethod(job); - genericMethod.Invoke(jobRegister, [recurringJobDetails.CronPattern, recurringJobDetails.Queue]); + genericMethod.Invoke(jobRegister, [recurringJobDetails.CronPattern, recurringJobDetails.Queue, ""]); } } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IJobManager.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IJobManager.cs index 15412e9..1a10f84 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IJobManager.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/IJobManager.cs @@ -20,12 +20,14 @@ public interface IJobManager IList GetEnqueuedJobs(Predicate predicate = null); IList GetProcessingJobs(Predicate predicate = null); IList GetFailedJobs(Predicate predicate = null); + IList GetJobs(Predicate predicate = null); + string Enqueue(TJobParams parameters, string queue = EnqueuedState.DefaultQueue) where TJob : IParameterizedAsyncJob; string Enqueue(string queue = EnqueuedState.DefaultQueue) where TJob : IAsyncJob; string Schedule(TimeSpan schedule, string queue = EnqueuedState.DefaultQueue) where TJob : IAsyncJob; string Schedule(TJobParams parameters, TimeSpan schedule, string queue = EnqueuedState.DefaultQueue) where TJob : IParameterizedAsyncJob; - void RegisterRecurring(string cron, string queue = EnqueuedState.DefaultQueue) where TJob : IAsyncJob; - void RegisterRecurring(string cron, TJobParams parameters, string queue = EnqueuedState.DefaultQueue) where TJob : IParameterizedAsyncJob; + void RegisterRecurring(string cron, string queue = EnqueuedState.DefaultQueue, string jobSuffix = "") where TJob : IAsyncJob; + void RegisterRecurring(TJobParams parameters, string cron, string queue = EnqueuedState.DefaultQueue, string jobSuffix = "") where TJob : IParameterizedAsyncJob; void DeleteRecurring(string id); void Delete(string id); } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncJob.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncJob.cs index ca79225..e3e7fa8 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncJob.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/AsyncJob.cs @@ -8,7 +8,7 @@ protected AsyncJob(ILogger logger) : base(logger) { } - public Task ExecuteAsync(CancellationToken jobCancellationToken) + public Task ExecuteAsync(CancellationToken jobCancellationToken = default) { return InternalExecuteAsync(async () => { diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IAsyncJob.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IAsyncJob.cs index c0da581..4ff0b53 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IAsyncJob.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/IAsyncJob.cs @@ -2,6 +2,6 @@ namespace PiBox.Plugins.Jobs.Hangfire.Job { public interface IAsyncJob : IDisposable { - Task ExecuteAsync(CancellationToken jobCancellationToken); + Task ExecuteAsync(CancellationToken jobCancellationToken = default); } } diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/ParameterizedAsyncJob.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/ParameterizedAsyncJob.cs index e873ab8..4ac53b6 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/ParameterizedAsyncJob.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/Job/ParameterizedAsyncJob.cs @@ -8,7 +8,7 @@ protected ParameterizedAsyncJob(ILogger logger) : base(logger) { } - public Task ExecuteAsync(T value, CancellationToken jobCancellationToken) + public Task ExecuteAsync(T value, CancellationToken jobCancellationToken = default) { return InternalExecuteAsync(async () => { diff --git a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobManager.cs b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobManager.cs index 6a652ed..94ea684 100644 --- a/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobManager.cs +++ b/PiBox.Plugins/Jobs/Hangfire/src/PiBox.Plugins.Jobs.Hangfire/JobManager.cs @@ -9,13 +9,23 @@ namespace PiBox.Plugins.Jobs.Hangfire { - internal class JobManager : IJobManager + public enum JobType { - private record PageOptions(int Offset, int PageSize) - { - public PageOptions Next() => this with { Offset = Offset + PageSize }; - } + Recurring, + Processing, + Enqueued, + Failed, + Fetched + } + public class JobEntity + { + public JobType Type { get; init; } + public global::Hangfire.Common.Job Job { get; init; } + public object JobDto { get; init; } + } + internal class JobManager : IJobManager + { private readonly IRecurringJobManager _recurringJobManager; private readonly IBackgroundJobClient _backgroundJobClient; private readonly bool _hasQueueSupport; @@ -47,9 +57,10 @@ private static string GetJobName(string jobIdSuffix = "") return name; } + public IList GetQueues() => MonitoringApi.Queues().Select(x => x.Name).ToList(); + public IList GetRecurringJobs() => Connection.GetRecurringJobs(); - public IList GetQueues() => MonitoringApi.Queues().Select(x => x.Name).ToList(); public IList GetEnqueuedJobs() => GetQueues() @@ -58,8 +69,7 @@ public IList GetEnqueuedJobs() => .ToList(); public IList GetProcessingJobs() => - GetQueues() - .SelectMany(queue => MonitoringApi.GetCompleteList((api, page) => api.ProcessingJobs(page.Offset, page.PageSize))) + MonitoringApi.GetCompleteList((api, page) => api.ProcessingJobs(page.Offset, page.PageSize)) .Select(x => x.Value) .ToList(); @@ -74,16 +84,21 @@ public IList GetFetchedJobs() => .Select(x => x.Value) .ToList(); - public IList GetJobs() + public IList GetJobs(Predicate predicate = null) { - var jobs = GetEnqueuedJobs().Select(x => x.Job).ToList(); - jobs.AddRange(GetProcessingJobs().Select(x => x.Job)); - jobs.AddRange(GetFailedJobs().Select(x => x.Job)); - jobs.AddRange(GetFetchedJobs().Select(x => x.Job)); - jobs.AddRange(GetRecurringJobs().Select(x => x.Job)); - return jobs; + IEnumerable all = [ + .. GetEnqueuedJobs().Select(x => CreateJobEntity(JobType.Enqueued, x.Job, x)), + .. GetProcessingJobs().Select(x => CreateJobEntity(JobType.Processing, x.Job, x)), + .. GetFailedJobs().Select(x => CreateJobEntity(JobType.Failed, x.Job, x)), + .. GetFetchedJobs().Select(x => CreateJobEntity(JobType.Fetched, x.Job, x)), + .. GetRecurringJobs().Select(x => CreateJobEntity(JobType.Recurring, x.Job, x)) + ]; + return predicate is null ? all.ToList() : all.Where(x => predicate(x)).ToList(); } + private static JobEntity CreateJobEntity(JobType jobType, global::Hangfire.Common.Job job, object jobDto) + => new() { Job = job, JobDto = jobDto, Type = jobType }; + public IList GetRecurringJobs(Predicate predicate = null) => GetRecurringJobs().Where(x => x.Job.Type == typeof(T) && (predicate == null || predicate(x))).ToList(); @@ -110,8 +125,8 @@ public string Enqueue(TJobParams parameters, string queue = En public string Enqueue(string queue = EnqueuedState.DefaultQueue) where TJob : IAsyncJob { return _hasQueueSupport - ? _backgroundJobClient.Enqueue(x => x.ExecuteAsync(CancellationToken.None)) - : _backgroundJobClient.Enqueue(queue, x => x.ExecuteAsync(CancellationToken.None)); + ? _backgroundJobClient.Enqueue(queue, x => x.ExecuteAsync(CancellationToken.None)) + : _backgroundJobClient.Enqueue(x => x.ExecuteAsync(CancellationToken.None)); } public string Schedule(TimeSpan schedule, string queue = EnqueuedState.DefaultQueue) @@ -130,22 +145,22 @@ public string Schedule(TJobParams parameters, TimeSpan schedul : _backgroundJobClient.Schedule(x => x.ExecuteAsync(parameters, CancellationToken.None), schedule); } - public void RegisterRecurring(string cron, string queue = EnqueuedState.DefaultQueue) + public void RegisterRecurring(string cron, string queue = EnqueuedState.DefaultQueue, string jobSuffix = "") where TJob : IAsyncJob { if (_hasQueueSupport) - _recurringJobManager.AddOrUpdate(GetJobName(), queue, x => x.ExecuteAsync(CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); + _recurringJobManager.AddOrUpdate(GetJobName(jobSuffix), queue, x => x.ExecuteAsync(CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); else - _recurringJobManager.AddOrUpdate(GetJobName(), x => x.ExecuteAsync(CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); + _recurringJobManager.AddOrUpdate(GetJobName(jobSuffix), x => x.ExecuteAsync(CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); } - public void RegisterRecurring(string cron, TJobParams parameters, - string queue = EnqueuedState.DefaultQueue) where TJob : IParameterizedAsyncJob + public void RegisterRecurring(TJobParams parameters, string cron, + string queue = EnqueuedState.DefaultQueue, string jobSuffix = "") where TJob : IParameterizedAsyncJob { if (_hasQueueSupport) - _recurringJobManager.AddOrUpdate(GetJobName(), queue, x => x.ExecuteAsync(parameters, CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); + _recurringJobManager.AddOrUpdate(GetJobName(jobSuffix), queue, x => x.ExecuteAsync(parameters, CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); else - _recurringJobManager.AddOrUpdate(GetJobName(), x => x.ExecuteAsync(parameters, CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); + _recurringJobManager.AddOrUpdate(GetJobName(jobSuffix), x => x.ExecuteAsync(parameters, CancellationToken.None), cron, new RecurringJobOptions { TimeZone = GetTimeZone() }); } public void DeleteRecurring(string id) => _recurringJobManager.RemoveIfExists(id); diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobTimeZoneAttributeTests.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobTimeZoneAttributeTests.cs new file mode 100644 index 0000000..d46591b --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobTimeZoneAttributeTests.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using NUnit.Framework; +using PiBox.Plugins.Jobs.Hangfire.Attributes; + +namespace PiBox.Plugins.Jobs.Hangfire.Tests.Attributes +{ + public class JobTimeZoneAttributeTests + { + [Test] + public void CanInitializeWithTimeZone() + { + var timeZoneAttribute = new JobTimeZoneAttribute(TimeZoneInfo.Utc); + timeZoneAttribute.TimeZoneInfo.Should().Be(TimeZoneInfo.Utc); + + timeZoneAttribute = new JobTimeZoneAttribute(TimeZoneInfo.Local); + timeZoneAttribute.TimeZoneInfo.Should().Be(TimeZoneInfo.Local); + } + } +} diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobTimeoutAttributeTests.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobTimeoutAttributeTests.cs new file mode 100644 index 0000000..a07c90d --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Attributes/JobTimeoutAttributeTests.cs @@ -0,0 +1,34 @@ +using FluentAssertions; +using NUnit.Framework; +using PiBox.Plugins.Jobs.Hangfire.Attributes; + +namespace PiBox.Plugins.Jobs.Hangfire.Tests.Attributes +{ + public class JobTimeoutAttributeTests + { + public record TestCase(int Value, TimeUnit Unit, TimeSpan Expected); + private static readonly TestCase[] _testCases = + [ + new(123, TimeUnit.Milliseconds, TimeSpan.FromMilliseconds(123)), + new(123, TimeUnit.Seconds, TimeSpan.FromSeconds(123)), + new(123, TimeUnit.Minutes, TimeSpan.FromMinutes(123)), + new(123, TimeUnit.Hours, TimeSpan.FromHours(123)), + new(123, TimeUnit.Days, TimeSpan.FromDays(123)), + ]; + + [Test, TestCaseSource(nameof(_testCases))] + public void CanInitialize(TestCase testCase) + { + var timeoutAttribute = new JobTimeoutAttribute(testCase.Value, testCase.Unit); + timeoutAttribute.Timeout.Should().Be(testCase.Expected); + } + + [Test] + public void ThrowsOnOutOfRangeValue() + { + var func = () => new JobTimeoutAttribute(123, (TimeUnit)100); + func.Invoking(x => x()) + .Should().Throw(); + } + } +} diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Extensions/MonitoringApiExtensionsTests.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Extensions/MonitoringApiExtensionsTests.cs new file mode 100644 index 0000000..1cf7a31 --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/Extensions/MonitoringApiExtensionsTests.cs @@ -0,0 +1,46 @@ +using FluentAssertions; +using Hangfire.Storage; +using Hangfire.Storage.Monitoring; +using NSubstitute; +using NUnit.Framework; +using PiBox.Plugins.Jobs.Hangfire.Extensions; + +namespace PiBox.Plugins.Jobs.Hangfire.Tests.Extensionms +{ + public class MonitoringApiExtensionsTests + { + private IMonitoringApi _api; + + [SetUp] + public void Up() + { + _api = Substitute.For(); + } + + private static JobList FakeJobList(int count) + { + var jobs = new FetchedJobDto[count]; + for (var i = 0; i < count; i++) + jobs[i] = new FetchedJobDto(); + var dic = jobs.Select(x => new KeyValuePair(Guid.NewGuid().ToString(), x)); + return new JobList(dic); + } + + [Test] + public void PollsThroughPagination() + { + var first = FakeJobList(500); + var second = FakeJobList(499); + _api.FetchedJobs(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(first, second); + + var result = _api.GetCompleteList((api, page) => api.FetchedJobs("default", page.Offset, page.PageSize)); + result.Count.Should().Be(first.Count + second.Count); + var keys = result.Select(x => x.Key).ToArray(); + foreach (var mockData in first.UnionBy(second, x => x.Key)) + { + keys.Should().Contain(mockData.Key); + } + } + } +} diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfirePluginTests.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfirePluginTests.cs index c16edc9..2802c01 100644 --- a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfirePluginTests.cs +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/HangfirePluginTests.cs @@ -31,12 +31,20 @@ public class HangfirePluginTests PollingIntervalInMs = 1000, WorkerCount = 200, EnableJobsByFeatureManagementConfig = true, - User = "testUser" + User = "testUser", + Queues = ["default", "test"] }; - private readonly IImplementationResolver _implementationResolver = Substitute.For(); + private IImplementationResolver _implementationResolver; + private IHangfireConfigurator _configurator; + private HangFirePlugin GetPlugin() => new(HangfireConfiguration, _implementationResolver, [_configurator]); - private HangFirePlugin GetPlugin() => new(HangfireConfiguration, _implementationResolver, []); + [SetUp] + public void Up() + { + _configurator = Substitute.For(); + _implementationResolver = Substitute.For(); + } [Test] public void ConfigureServiceTest() @@ -49,13 +57,23 @@ public void ConfigureServiceTest() var hangfireConfiguration = sp.GetRequiredService(); hangfireConfiguration.Should().NotBeNull(); + + var backgroundJobServerOptions = sp.GetServices(); + backgroundJobServerOptions.Should().NotBeNull(); + + var jobManager = sp.GetService(); + jobManager.Should().NotBeNull(); + jobManager.Should().BeOfType(); + + _configurator.Received(1).Configure(Arg.Any()); + _configurator.Received(1).ConfigureServer(Arg.Any()); } [Test] public void ConfigureApplicationTest() { _implementationResolver.FindTypes() - .Returns(new List { typeof(TestJobTimeoutAsync) }); + .Returns([typeof(TestJobTimeoutAsync)]); JobStorage.Current = new MemoryStorage(); var sc = new ServiceCollection(); sc.AddSingleton(NullLoggerFactory.Instance); diff --git a/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/JobManagerTests.cs b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/JobManagerTests.cs new file mode 100644 index 0000000..b4767cc --- /dev/null +++ b/PiBox.Plugins/Jobs/Hangfire/test/PiBox.Plugins.Jobs.Hangfire.Tests/JobManagerTests.cs @@ -0,0 +1,314 @@ +using System.Collections.Immutable; +using FluentAssertions; +using Hangfire; +using Hangfire.Common; +using Hangfire.States; +using Hangfire.Storage; +using Hangfire.Storage.Monitoring; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NUnit.Framework; +using PiBox.Hosting.Abstractions.Extensions; +using PiBox.Plugins.Jobs.Hangfire.Job; + +namespace PiBox.Plugins.Jobs.Hangfire.Tests +{ + public class JobManagerTests + { + private static readonly global::Hangfire.Common.Job _testJob = new(typeof(TestJob), typeof(TestJob).GetMethod("ExecuteAsync"), new List { CancellationToken.None }); + private IRecurringJobManager _recurringJobManager; + private IBackgroundJobClient _backgroundJobClient; + private IStorageConnection _storageConnection; + private IMonitoringApi _monitoringApi; + private JobManager GetJobManager(bool hasQueueSupport = false) => + new(hasQueueSupport, _storageConnection, _monitoringApi, _recurringJobManager, _backgroundJobClient); + + [SetUp] + public void Up() + { + _recurringJobManager = Substitute.For(); + _backgroundJobClient = Substitute.For(); + _storageConnection = Substitute.For(); + _monitoringApi = Substitute.For(); + } + + [Test] + public void GetQueuesWorks() + { + _monitoringApi.Queues().Returns([new QueueWithTopEnqueuedJobsDto { Name = "test" }]); + var result = GetJobManager().GetQueues(); + result.Should().HaveCount(1).And.Contain("test"); + } + + [Test] + public void GetRecurringJobsWorks() + { + _storageConnection.GetAllItemsFromSet("recurring-jobs") + .Returns(["test"]); + _storageConnection.GetAllEntriesFromHash("recurring-job:test") + .Returns(new Dictionary + { + { "Cron", "* * * * *" }, + { "Job", new { t = TypeHelper.CurrentTypeSerializer(typeof(TestJob)), m = nameof(TestJob.ExecuteAsync), p = new List {TypeHelper.CurrentTypeSerializer(typeof(CancellationToken))}, a = new string[] {null} }.Serialize() } + }); + var result = GetJobManager().GetRecurringJobs(); + result.Should().HaveCount(1).And.Contain(x => x.Id == "test"); + + result = GetJobManager().GetRecurringJobs(); + result.Should().HaveCount(1).And.Contain(x => x.Id == "test"); + result = GetJobManager().GetRecurringJobs(x => x.Id == "test"); + result.Should().HaveCount(1).And.Contain(x => x.Id == "test"); + result = GetJobManager().GetRecurringJobs(x => x.Id != "test"); + result.Should().HaveCount(0); + } + [Test] + public void GetEnqueuedJobsWorks() + { + var dto = new EnqueuedJobDto { Job = _testJob }; + _monitoringApi.Queues().Returns([new QueueWithTopEnqueuedJobsDto { Name = "test" }]); + _monitoringApi.EnqueuedJobs("test", 0, 500) + .Returns(new JobList(new List> + { + new("test1", dto) + })); + var result = GetJobManager().GetEnqueuedJobs(); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetEnqueuedJobs(); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetEnqueuedJobs(x => x.Job.Method.Name == "ExecuteAsync"); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetEnqueuedJobs(x => x.Job.Method.Name == "Run"); + result.Should().HaveCount(0); + } + + [Test] + public void GetProcessingJobsWorks() + { + var dto = new ProcessingJobDto { Job = _testJob }; + _monitoringApi.ProcessingJobs(0, 500) + .Returns(new JobList(new List> + { + new("test1", dto) + })); + var result = GetJobManager().GetProcessingJobs(); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetProcessingJobs(); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetProcessingJobs(x => x.Job.Method.Name == "ExecuteAsync"); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetProcessingJobs(x => x.Job.Method.Name == "Run"); + result.Should().HaveCount(0); + } + + [Test] + public void GetFailedJobsWorks() + { + var dto = new FailedJobDto { Job = _testJob }; + _monitoringApi.FailedJobs(0, 500) + .Returns(new JobList(new List> + { + new("test1", dto) + })); + var result = GetJobManager().GetFailedJobs(); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetFailedJobs(); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetFailedJobs(x => x.Job.Method.Name == "ExecuteAsync"); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetFailedJobs(x => x.Job.Method.Name == "Run"); + result.Should().HaveCount(0); + } + + [Test] + public void GetFetchedJobsWorks() + { + var dto = new FetchedJobDto { Job = _testJob }; + _monitoringApi.Queues().Returns([new QueueWithTopEnqueuedJobsDto { Name = "test" }]); + _monitoringApi.FetchedJobs("test", 0, 500) + .Returns(new JobList(new List> + { + new("test1", dto) + })); + var result = GetJobManager().GetFetchedJobs(); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetFetchedJobs(); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetFetchedJobs(x => x.Job.Method.Name == "ExecuteAsync"); + result.Should().HaveCount(1).And.Contain(dto); + + result = GetJobManager().GetFetchedJobs(x => x.Job.Method.Name == "Run"); + result.Should().HaveCount(0); + } + + [Test] + public void GetJobsWorks() + { + _monitoringApi.Queues().Returns([new QueueWithTopEnqueuedJobsDto { Name = "test" }]); + _monitoringApi.FetchedJobs("test", 0, 500) + .Returns(new JobList(new List> + { + new("test1", new FetchedJobDto {Job = _testJob}) + })); + _monitoringApi.FailedJobs(0, 500) + .Returns(new JobList(new List> + { + new("test1", new FailedJobDto {Job = _testJob}) + })); + _monitoringApi.ProcessingJobs(0, 500) + .Returns(new JobList(new List> + { + new("testProc", new ProcessingJobDto {Job = _testJob}) + })); + _monitoringApi.EnqueuedJobs("test", 0, 500) + .Returns(new JobList(new List> + { + new("testEnq", new EnqueuedJobDto {Job = _testJob}) + })); + _storageConnection.GetAllItemsFromSet("recurring-jobs") + .Returns(["test"]); + _storageConnection.GetAllEntriesFromHash("recurring-job:test") + .Returns(new Dictionary + { + { "Cron", "* * * * *" }, + { "Job", new { t = TypeHelper.CurrentTypeSerializer(typeof(TestJob)), m = nameof(TestJob.ExecuteAsync), p = new List {TypeHelper.CurrentTypeSerializer(typeof(CancellationToken))}, a = new string[] {null} }.Serialize() } + }); + var result = GetJobManager().GetJobs(); + result.Should().HaveCount(Enum.GetValues(typeof(JobType)).Length); + foreach (var val in Enum.GetValues(typeof(JobType)).OfType()) + { + result.Should().Contain(x => x.Type == val && x.Job.Type == typeof(TestJob) && x.JobDto != null); + } + result = GetJobManager().GetJobs(x => x.Type == JobType.Enqueued); + result.Should().HaveCount(1); + result[0].Type.Should().Be(JobType.Enqueued); + result[0].JobDto.Should().BeOfType(); + result[0].Job.Should().Be(_testJob); + } + + [Test] + public void EnqueueWorks() + { + GetJobManager().Enqueue(); + _backgroundJobClient.Received(1).Create(Arg.Is(j => j.Type == typeof(TestJob)), + Arg.Any()); + + GetJobManager(true).Enqueue("test"); + _backgroundJobClient.Received(1).Create(Arg.Is(j => j.Type == typeof(TestJob) && j.Queue == "test"), + Arg.Any()); + + GetJobManager().Enqueue("123"); + _backgroundJobClient.Received(1).Create(Arg.Is(j => j.Type == typeof(TestParamJob) + && j.Args != null && j.Args[0].ToString() == "123"), + Arg.Any()); + + GetJobManager(true).Enqueue("123", "test"); + _backgroundJobClient.Received(1).Create(Arg.Is(j => j.Type == typeof(TestParamJob) + && j.Args != null && j.Args[0].ToString() == "123" && j.Queue == "test"), + Arg.Any()); + } + + [Test] + public void ScheduleWorks() + { + var ts = TimeSpan.FromDays(1); + var date = DateTime.UtcNow.Add(ts).Date; + GetJobManager().Schedule(ts); + _backgroundJobClient.Received(1).Create(Arg.Is(j => j.Type == typeof(TestJob)), + Arg.Is(s => s.EnqueueAt.Date.Equals(date.Date))); + + GetJobManager(true).Schedule(ts, "test"); + _backgroundJobClient.Received(1).Create(Arg.Is(j => j.Type == typeof(TestJob) && j.Queue == "test"), + Arg.Is(s => s.EnqueueAt.Date.Equals(date.Date))); + + GetJobManager().Schedule("123", ts); + _backgroundJobClient.Received(1).Create(Arg.Is(j => j.Type == typeof(TestParamJob) + && j.Args != null && j.Args[0].ToString() == "123"), + Arg.Is(s => s.EnqueueAt.Date.Equals(date.Date))); + + GetJobManager(true).Schedule("123", ts, "test"); + _backgroundJobClient.Received(1).Create(Arg.Is(j => j.Type == typeof(TestParamJob) + && j.Args != null && j.Args[0].ToString() == "123" && j.Queue == "test"), + Arg.Is(s => s.EnqueueAt.Date.Equals(date.Date))); + } + + [Test] + public void RegisterRecurringWorks() + { + const string Cron = "* * * * *"; + GetJobManager().RegisterRecurring(Cron, jobSuffix: "suffix"); + _recurringJobManager.Received(1).AddOrUpdate( + Arg.Is(s => s.Contains("Test") && s.Contains("suffix")), + Arg.Is(j => j.Type == typeof(TestJob)), + Cron, + Arg.Is(r => r.TimeZone == TimeZoneInfo.Utc) + ); + + GetJobManager(true).RegisterRecurring(Cron, "test", "suffix"); + _recurringJobManager.Received(1).AddOrUpdate( + Arg.Is(s => s.Contains("Test") && s.Contains("suffix")), + Arg.Is(j => j.Type == typeof(TestJob) && j.Queue == "test"), + Cron, + Arg.Is(r => r.TimeZone == TimeZoneInfo.Utc) + ); + + GetJobManager().RegisterRecurring("123", Cron, jobSuffix: "suffix"); + _recurringJobManager.Received(1).AddOrUpdate( + Arg.Is(s => s.Contains("TestParam") && s.Contains("suffix")), + Arg.Is(j => j.Type == typeof(TestParamJob) && j.Args != null && j.Args[0].ToString() == "123"), + Cron, + Arg.Is(r => r.TimeZone == TimeZoneInfo.Utc) + ); + + GetJobManager(true).RegisterRecurring("123", Cron, "test", "suffix"); + _recurringJobManager.Received(1).AddOrUpdate( + Arg.Is(s => s.Contains("TestParam") && s.Contains("suffix")), + Arg.Is(j => j.Type == typeof(TestParamJob) && j.Args != null && j.Args[0].ToString() == "123" && j.Queue == "test"), + Cron, + Arg.Is(r => r.TimeZone == TimeZoneInfo.Utc) + ); + } + + [Test] + public void DeleteWorks() + { + GetJobManager().Delete("123"); + _backgroundJobClient.Received(1).ChangeState("123", Arg.Any(), Arg.Any()); + } + + [Test] + public void DeleteRecurringWorks() + { + GetJobManager().DeleteRecurring("123"); + _recurringJobManager.Received(1).RemoveIfExists("123"); + } + } + + public class TestJob : AsyncJob + { + public TestJob(ILogger logger) : base(logger) { } + protected override Task ExecuteJobAsync(CancellationToken cancellationToken) + { + return Task.FromResult(new { test = true }); + } + } + + public class TestParamJob : ParameterizedAsyncJob + { + public TestParamJob(ILogger logger) : base(logger) { } + protected override Task ExecuteJobAsync(string value, CancellationToken cancellationToken) + { + return Task.FromResult(new { test = true, Val = value }); + } + } +} diff --git a/example/src/PiBox.Example.Service/TestConfigurator.cs b/example/src/PiBox.Example.Service/TestConfigurator.cs deleted file mode 100644 index 67d47f5..0000000 --- a/example/src/PiBox.Example.Service/TestConfigurator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Hangfire; -using Hangfire.MemoryStorage; -using PiBox.Plugins.Jobs.Hangfire; - -namespace PiBox.Example.Service -{ - public class TestConfigurator : IHangfireConfigurator - { - public bool IncludesStorage => false; - - public void Configure(IGlobalConfiguration config) - { - config.UseMemoryStorage(); - } - - public void ConfigureServer(BackgroundJobServerOptions options) - { - options.WorkerCount = 1; - } - } -} diff --git a/example/src/PiBox.Example.Service/TestHangfire.cs b/example/src/PiBox.Example.Service/TestHangfire.cs new file mode 100644 index 0000000..e180d04 --- /dev/null +++ b/example/src/PiBox.Example.Service/TestHangfire.cs @@ -0,0 +1,36 @@ +using Hangfire; +using Hangfire.MemoryStorage; +using PiBox.Plugins.Jobs.Hangfire; +using PiBox.Plugins.Jobs.Hangfire.Attributes; +using PiBox.Plugins.Jobs.Hangfire.Job; + +namespace PiBox.Example.Service +{ + public class TestHangfire : IHangfireConfigurator + { + public bool IncludesStorage => false; + + public void Configure(IGlobalConfiguration config) + { + config.UseMemoryStorage(); + } + + public void ConfigureServer(BackgroundJobServerOptions options) + { + options.WorkerCount = 1; + } + } + + [RecurringJob("*/1 * * * *")] + public class TestJob : AsyncJob + { + public TestJob(ILogger logger) : base(logger) + { + } + + protected override Task ExecuteJobAsync(CancellationToken cancellationToken) + { + return Task.FromResult(new { Hello = "World" }); + } + } +} From 8b9f149f3dfb3737ca13ef3c74433146e96d3f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=BCrzd=C3=B6rfer?= Date: Tue, 29 Oct 2024 12:23:54 +0100 Subject: [PATCH 3/5] add missing test --- .../Services/TypeImplementationResolver.cs | 2 +- .../TypeImplementationResolverTests.cs | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs b/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs index 7adfcc0..e1a88ed 100644 --- a/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs +++ b/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs @@ -39,7 +39,7 @@ private object GetArgument(Type instanceType, Type type) return GetConfiguration(type, type.GetAttribute()!.Section); var isList = type.GetInterfaces().Any(i => i.IsAssignableTo(typeof(IEnumerable))); - var innerType = isList ? type.GetElementType()! : type; + var innerType = !isList ? type : type.GetElementType() ?? type.GenericTypeArguments[0]!; if (innerType.IsInterface && innerType.IsAssignableTo(typeof(IPluginConfigurator))) { diff --git a/PiBox.Hosting/WebHost/test/PiBox.Hosting.WebHost.Tests/TypeImplementationResolverTests.cs b/PiBox.Hosting/WebHost/test/PiBox.Hosting.WebHost.Tests/TypeImplementationResolverTests.cs index 3c2d8af..22f3076 100644 --- a/PiBox.Hosting/WebHost/test/PiBox.Hosting.WebHost.Tests/TypeImplementationResolverTests.cs +++ b/PiBox.Hosting/WebHost/test/PiBox.Hosting.WebHost.Tests/TypeImplementationResolverTests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using PiBox.Hosting.Abstractions.Attributes; using PiBox.Hosting.Abstractions.Extensions; +using PiBox.Hosting.Abstractions.Plugins; using PiBox.Hosting.Abstractions.Services; using PiBox.Hosting.WebHost.Services; using PiBox.Testing; @@ -83,6 +84,36 @@ public void CanResolvePluginConfigurations() pluginConfig!.Name.Should().Be(configName); } + [Test] + public void CanResolveConfigurators() + { + var resolver = new TypeImplementationResolver(_configuration, _resolvedTypes, new Dictionary()); + var instance = resolver.ResolveInstance(typeof(Configurator)) as Configurator; + instance.Should().NotBeNull(); + instance!.GetMessage().Should().Be("Hello World!"); + + var plugin = resolver.ResolveInstance(typeof(ConfiguratorPlugin)) as ConfiguratorPlugin; + plugin.Should().NotBeNull(); + plugin!.Message.Should().Be("Hello World!"); + plugin!.Message2.Should().Be("Hello World!"); + } + + internal class Configurator : IConfiguratorPluginConfigurator + { + public string GetMessage() => "Hello World!"; + } + + internal interface IConfiguratorPluginConfigurator : IPluginConfigurator + { + string GetMessage(); + } + + internal class ConfiguratorPlugin(IConfiguratorPluginConfigurator[] configurators, IList configurators2) : IPluginActivateable + { + public string Message = string.Join(" ", configurators.Select(c => c.GetMessage())); + public string Message2 = string.Join(" ", configurators2.Select(c => c.GetMessage())); + } + [Configuration("sampleConfig")] internal class UnitTestPluginConfig { From b63c2a00d0aff854da4af114b1e8252555b0e8f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=BCrzd=C3=B6rfer?= Date: Wed, 30 Oct 2024 10:27:31 +0100 Subject: [PATCH 4/5] review tasks, enhance GetInaccessibleValue for base types --- .../Services/TypeImplementationResolver.cs | 2 -- .../TypeImplementationResolverTests.cs | 17 ++++++++--------- .../Extensions/ObjectExtensions.cs | 17 +++++++++++------ 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs b/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs index e1a88ed..0b8d9a6 100644 --- a/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs +++ b/PiBox.Hosting/WebHost/src/PiBox.Hosting.WebHost/Services/TypeImplementationResolver.cs @@ -51,8 +51,6 @@ private object GetArgument(Type instanceType, Type type) .Where(x => x.IsClass && !x.IsAbstract && x.IsAssignableTo(innerType)) .Select(ResolveInstance) .ToList(); - if (!isList) - return args.FirstOrDefault(); if (type.IsArray) { diff --git a/PiBox.Hosting/WebHost/test/PiBox.Hosting.WebHost.Tests/TypeImplementationResolverTests.cs b/PiBox.Hosting/WebHost/test/PiBox.Hosting.WebHost.Tests/TypeImplementationResolverTests.cs index 22f3076..208425a 100644 --- a/PiBox.Hosting/WebHost/test/PiBox.Hosting.WebHost.Tests/TypeImplementationResolverTests.cs +++ b/PiBox.Hosting/WebHost/test/PiBox.Hosting.WebHost.Tests/TypeImplementationResolverTests.cs @@ -13,7 +13,7 @@ namespace PiBox.Hosting.WebHost.Tests { public class TypeImplementationResolverTests { - private readonly Type[] _resolvedTypes = new Type[] { typeof(SampleType), typeof(WithoutCtor), typeof(UnitTestPluginConfig) }; + private readonly Type[] _resolvedTypes = [typeof(SampleType), typeof(WithoutCtor), typeof(UnitTestPluginConfig)]; private readonly IConfiguration _configuration = Substitute.For(); [Test] @@ -98,17 +98,17 @@ public void CanResolveConfigurators() plugin!.Message2.Should().Be("Hello World!"); } - internal class Configurator : IConfiguratorPluginConfigurator + private class Configurator : IConfiguratorPluginConfigurator { public string GetMessage() => "Hello World!"; } - internal interface IConfiguratorPluginConfigurator : IPluginConfigurator + private interface IConfiguratorPluginConfigurator : IPluginConfigurator { string GetMessage(); } - internal class ConfiguratorPlugin(IConfiguratorPluginConfigurator[] configurators, IList configurators2) : IPluginActivateable + private class ConfiguratorPlugin(IConfiguratorPluginConfigurator[] configurators, IList configurators2) : IPluginActivateable { public string Message = string.Join(" ", configurators.Select(c => c.GetMessage())); public string Message2 = string.Join(" ", configurators2.Select(c => c.GetMessage())); @@ -120,11 +120,10 @@ internal class UnitTestPluginConfig public string Name { get; set; } = null!; } - internal abstract class BaseClass - { - } + private abstract class BaseClass; + #pragma warning disable S3881 - internal class SampleType : BaseClass, IDisposable + private class SampleType : BaseClass, IDisposable { public static int CreationCount; public static int DisposeCount; @@ -152,7 +151,7 @@ protected virtual void Dispose(bool disposing) } #pragma warning restore S3881 - internal class WithoutCtor + private class WithoutCtor { private readonly string Test = "TEST"; public string GetTest() => Test; diff --git a/PiBox.Testing/NUnit/src/PiBox.Testing/Extensions/ObjectExtensions.cs b/PiBox.Testing/NUnit/src/PiBox.Testing/Extensions/ObjectExtensions.cs index 6dd6d64..d903ac7 100644 --- a/PiBox.Testing/NUnit/src/PiBox.Testing/Extensions/ObjectExtensions.cs +++ b/PiBox.Testing/NUnit/src/PiBox.Testing/Extensions/ObjectExtensions.cs @@ -6,12 +6,17 @@ public static class ObjectExtensions { public static T GetInaccessibleValue(this object obj, string name) { - var field = obj.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); - if (field is not null) - return (T)field.GetValue(obj)!; - var property = obj.GetType().GetProperty(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); - if (property is not null) - return (T)property.GetValue(obj)!; + var type = obj.GetType(); + while (type is not null) + { + var field = type.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); + if (field is not null) + return (T)field.GetValue(obj)!; + var property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); + if (property is not null) + return (T)property.GetValue(obj)!; + type = type.BaseType; + } throw new ArgumentException($"Could not find field or property '{name}'"); } } From a9d7708d39da74bd6a1cd7d065442308a3a5a583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20K=C3=BCrzd=C3=B6rfer?= Date: Mon, 25 Nov 2024 12:37:50 +0100 Subject: [PATCH 5/5] fix interface bug for db health checks --- .../EntityFrameworkPlugin.cs | 2 +- .../EntityFrameworkPluginTests.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/EntityFrameworkPlugin.cs b/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/EntityFrameworkPlugin.cs index c63af55..f90dc25 100644 --- a/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/EntityFrameworkPlugin.cs +++ b/PiBox.Plugins/Persistence/EntityFramework/src/PiBox.Plugins.Persistence.EntityFramework/EntityFrameworkPlugin.cs @@ -38,7 +38,7 @@ public void ConfigureApplication(IApplicationBuilder applicationBuilder) public void ConfigureHealthChecks(IHealthChecksBuilder healthChecksBuilder) { var dbContexts = implementationResolver.FindAssemblies().SelectMany(x => x.GetTypes()) - .Where(x => x.GetInterfaces().Any(i => i == typeof(IDbContext))) + .Where(x => x.IsClass && !x.IsAbstract && x.IsAssignableTo(typeof(IDbContext))) .ToList(); var registerHc = typeof(DependencyInjectionExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public) .Single(x => x.GetParameters().Length == 1 && x.GetParameters()[0].ParameterType == typeof(IHealthChecksBuilder)); diff --git a/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/EntityFrameworkPluginTests.cs b/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/EntityFrameworkPluginTests.cs index 2101717..0133127 100644 --- a/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/EntityFrameworkPluginTests.cs +++ b/PiBox.Plugins/Persistence/EntityFramework/test/PiBox.Plugins.Persistence.EntityFramework.Tests/EntityFrameworkPluginTests.cs @@ -67,6 +67,7 @@ public void ConfiguresHealthChecks() _implementationResolver.FindAssemblies().Returns([typeof(EntityFrameworkPluginTests).Assembly]); var healthCheckBuilder = Substitute.For(); _plugin.ConfigureHealthChecks(healthCheckBuilder); + healthCheckBuilder.Received(1).Add(Arg.Any()); healthCheckBuilder.Received(1).Add(Arg.Is(h => h.Name == nameof(TestContext))); }