Skip to content

Commit c43458a

Browse files
committed
Add dynamic background job support
Introduce dynamic background job support so jobs can be registered and executed without a compile-time job type. Key changes: - Add DynamicBackgroundJobContext, IDynamicBackgroundJobHandlerProvider and DynamicBackgroundJobHandlerProvider to allow registering/unregistering dynamic handlers at runtime. - Extend BackgroundJobConfiguration with DynamicHandler and IsDynamic, and make JobType nullable for dynamic scenarios. - Update AbpBackgroundJobOptions to use a ConcurrentDictionary for name lookup, and add methods to Add/Remove dynamic jobs and GetJobOrNull. - Extend JobExecutionContext with JobName and propagate it through Hangfire/Quartz/RabbitMQ/TickerQ adapters and worker code. - Update BackgroundJobExecuter to detect and execute dynamic handlers, deserialize/ensure dictionary args, and retain existing typed execution path. - Add tests (DynamicJobExecutionTracker, runtime/compile-time dynamic handler tests) and register a sample dynamic job in test module. - Update demo SampleJobCreator and DemoAppSharedModule to demonstrate compile-time and runtime dynamic job registration and enqueueing. These changes enable flexible, dictionary-based job arguments and runtime registration of background job handlers while preserving existing typed job execution.
1 parent e615b31 commit c43458a

18 files changed

Lines changed: 390 additions & 39 deletions

File tree

framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AbpBackgroundJobOptions.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
using System;
1+
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.Collections.Immutable;
5+
using System.Threading.Tasks;
46

57
namespace Volo.Abp.BackgroundJobs;
68

79
public class AbpBackgroundJobOptions
810
{
911
private readonly Dictionary<Type, BackgroundJobConfiguration> _jobConfigurationsByArgsType;
10-
private readonly Dictionary<string, BackgroundJobConfiguration> _jobConfigurationsByName;
12+
private readonly ConcurrentDictionary<string, BackgroundJobConfiguration> _jobConfigurationsByName;
1113

1214
/// <summary>
1315
/// Default: true.
@@ -23,7 +25,7 @@ public class AbpBackgroundJobOptions
2325
public AbpBackgroundJobOptions()
2426
{
2527
_jobConfigurationsByArgsType = new Dictionary<Type, BackgroundJobConfiguration>();
26-
_jobConfigurationsByName = new Dictionary<string, BackgroundJobConfiguration>();
28+
_jobConfigurationsByName = new ConcurrentDictionary<string, BackgroundJobConfiguration>();
2729
GetBackgroundJobName = BackgroundJobNameAttribute.GetName;
2830
}
2931

@@ -46,7 +48,7 @@ public BackgroundJobConfiguration GetJob(Type argsType)
4648

4749
public BackgroundJobConfiguration GetJob(string name)
4850
{
49-
var jobConfiguration = _jobConfigurationsByName.GetOrDefault(name);
51+
var jobConfiguration = GetJobOrNull(name);
5052

5153
if (jobConfiguration == null)
5254
{
@@ -56,6 +58,11 @@ public BackgroundJobConfiguration GetJob(string name)
5658
return jobConfiguration;
5759
}
5860

61+
public BackgroundJobConfiguration? GetJobOrNull(string name)
62+
{
63+
return _jobConfigurationsByName.GetValueOrDefault(name);
64+
}
65+
5966
public IReadOnlyList<BackgroundJobConfiguration> GetJobs()
6067
{
6168
return _jobConfigurationsByArgsType.Values.ToImmutableList();
@@ -76,4 +83,29 @@ public void AddJob(BackgroundJobConfiguration jobConfiguration)
7683
_jobConfigurationsByArgsType[jobConfiguration.ArgsType] = jobConfiguration;
7784
_jobConfigurationsByName[jobConfiguration.JobName] = jobConfiguration;
7885
}
86+
87+
public void AddDynamicJob(string jobName, Func<DynamicBackgroundJobContext, Task> handler)
88+
{
89+
var config = new BackgroundJobConfiguration(jobName, handler);
90+
_jobConfigurationsByName[jobName] = config;
91+
}
92+
93+
public void AddDynamicJob(string jobName, Action<DynamicBackgroundJobContext> handler)
94+
{
95+
AddDynamicJob(jobName, context =>
96+
{
97+
handler(context);
98+
return Task.CompletedTask;
99+
});
100+
}
101+
102+
public bool RemoveDynamicJob(string name)
103+
{
104+
if (_jobConfigurationsByName.TryGetValue(name, out var config) && config.IsDynamic)
105+
{
106+
return _jobConfigurationsByName.TryRemove(name, out _);
107+
}
108+
109+
return false;
110+
}
79111
}
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
1-
using System;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
24

35
namespace Volo.Abp.BackgroundJobs;
46

57
public class BackgroundJobConfiguration
68
{
79
public Type ArgsType { get; }
810

9-
public Type JobType { get; }
11+
public Type? JobType { get; }
1012

1113
public string JobName { get; }
1214

15+
public bool IsDynamic { get; }
16+
17+
public Func<DynamicBackgroundJobContext, Task>? DynamicHandler { get; }
18+
1319
public BackgroundJobConfiguration(Type jobType, string jobName)
1420
{
1521
JobType = jobType;
1622
ArgsType = BackgroundJobArgsHelper.GetJobArgsType(jobType);
1723
JobName = jobName;
1824
}
25+
26+
public BackgroundJobConfiguration(string jobName, Func<DynamicBackgroundJobContext, Task> handler)
27+
{
28+
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
29+
Check.NotNull(handler, nameof(handler));
30+
31+
JobName = jobName;
32+
DynamicHandler = handler;
33+
IsDynamic = true;
34+
ArgsType = typeof(Dictionary<string, object>);
35+
}
1936
}

framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
using Microsoft.Extensions.Logging;
1+
using Microsoft.Extensions.Logging;
22
using Microsoft.Extensions.Logging.Abstractions;
33
using Microsoft.Extensions.Options;
44
using System;
5+
using System.Collections.Generic;
6+
using System.Text.Json;
57
using System.Threading.Tasks;
68
using Microsoft.Extensions.DependencyInjection;
79
using Volo.Abp.DependencyInjection;
@@ -16,7 +18,7 @@ public class BackgroundJobExecuter : IBackgroundJobExecuter, ITransientDependenc
1618
public ILogger<BackgroundJobExecuter> Logger { protected get; set; }
1719

1820
protected AbpBackgroundJobOptions Options { get; }
19-
21+
2022
protected ICurrentTenant CurrentTenant { get; }
2123

2224
public BackgroundJobExecuter(IOptions<AbpBackgroundJobOptions> options, ICurrentTenant currentTenant)
@@ -28,6 +30,56 @@ public BackgroundJobExecuter(IOptions<AbpBackgroundJobOptions> options, ICurrent
2830
}
2931

3032
public virtual async Task ExecuteAsync(JobExecutionContext context)
33+
{
34+
if (context.JobName != null)
35+
{
36+
var jobConfig = Options.GetJobOrNull(context.JobName);
37+
if (jobConfig?.DynamicHandler != null)
38+
{
39+
await ExecuteDynamicHandlerAsync(context, jobConfig);
40+
return;
41+
}
42+
}
43+
44+
await ExecuteTypedHandlerAsync(context);
45+
}
46+
47+
protected virtual async Task ExecuteDynamicHandlerAsync(JobExecutionContext context, BackgroundJobConfiguration jobConfig)
48+
{
49+
try
50+
{
51+
var cancellationTokenProvider =
52+
context.ServiceProvider.GetRequiredService<ICancellationTokenProvider>();
53+
54+
using (cancellationTokenProvider.Use(context.CancellationToken))
55+
{
56+
var dictArgs = EnsureDictionaryArgs(context.JobArgs);
57+
var dynamicContext = new DynamicBackgroundJobContext(
58+
context.ServiceProvider,
59+
dictArgs,
60+
context.CancellationToken
61+
);
62+
63+
await jobConfig.DynamicHandler!(dynamicContext);
64+
}
65+
}
66+
catch (Exception ex)
67+
{
68+
Logger.LogException(ex);
69+
70+
await context.ServiceProvider
71+
.GetRequiredService<IExceptionNotifier>()
72+
.NotifyAsync(new ExceptionNotificationContext(ex));
73+
74+
throw new BackgroundJobExecutionException("A background job execution is failed. See inner exception for details.", ex)
75+
{
76+
JobType = context.JobName!,
77+
JobArgs = context.JobArgs
78+
};
79+
}
80+
}
81+
82+
protected virtual async Task ExecuteTypedHandlerAsync(JobExecutionContext context)
3183
{
3284
var job = context.ServiceProvider.GetService(context.JobType);
3385
if (job == null)
@@ -45,7 +97,7 @@ public virtual async Task ExecuteAsync(JobExecutionContext context)
4597

4698
try
4799
{
48-
using(CurrentTenant.Change(GetJobArgsTenantId(context.JobArgs)))
100+
using (CurrentTenant.Change(GetJobArgsTenantId(context.JobArgs)))
49101
{
50102
var cancellationTokenProvider =
51103
context.ServiceProvider.GetRequiredService<ICancellationTokenProvider>();
@@ -54,15 +106,14 @@ public virtual async Task ExecuteAsync(JobExecutionContext context)
54106
{
55107
if (jobExecuteMethod.Name == nameof(IAsyncBackgroundJob<object>.ExecuteAsync))
56108
{
57-
await ((Task)jobExecuteMethod.Invoke(job, new[] { context.JobArgs })!);
109+
await ((Task)jobExecuteMethod.Invoke(job, [context.JobArgs])!);
58110
}
59111
else
60112
{
61-
jobExecuteMethod.Invoke(job, new[] { context.JobArgs });
113+
jobExecuteMethod.Invoke(job, [context.JobArgs]);
62114
}
63115
}
64116
}
65-
66117
}
67118
catch (Exception ex)
68119
{
@@ -79,7 +130,25 @@ await context.ServiceProvider
79130
};
80131
}
81132
}
82-
133+
134+
protected virtual Dictionary<string, object> EnsureDictionaryArgs(object jobArgs)
135+
{
136+
if (jobArgs is Dictionary<string, object> dict)
137+
{
138+
return dict;
139+
}
140+
141+
if (jobArgs is JsonElement jsonElement)
142+
{
143+
return JsonSerializer.Deserialize<Dictionary<string, object>>(jsonElement.GetRawText())
144+
?? new Dictionary<string, object>();
145+
}
146+
147+
var json = JsonSerializer.Serialize(jobArgs);
148+
return JsonSerializer.Deserialize<Dictionary<string, object>>(json)
149+
?? new Dictionary<string, object>();
150+
}
151+
83152
protected virtual Guid? GetJobArgsTenantId(object jobArgs)
84153
{
85154
return jobArgs switch
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using Volo.Abp.DependencyInjection;
5+
6+
namespace Volo.Abp.BackgroundJobs;
7+
8+
public class DynamicBackgroundJobContext : IServiceProviderAccessor
9+
{
10+
public IServiceProvider ServiceProvider { get; }
11+
12+
public Dictionary<string, object> Args { get; }
13+
14+
public CancellationToken CancellationToken { get; }
15+
16+
public DynamicBackgroundJobContext(
17+
IServiceProvider serviceProvider,
18+
Dictionary<string, object> args,
19+
CancellationToken cancellationToken = default)
20+
{
21+
ServiceProvider = serviceProvider;
22+
Args = args;
23+
CancellationToken = cancellationToken;
24+
}
25+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Microsoft.Extensions.Options;
4+
using Volo.Abp.DependencyInjection;
5+
6+
namespace Volo.Abp.BackgroundJobs;
7+
8+
public class DynamicBackgroundJobHandlerProvider : IDynamicBackgroundJobHandlerProvider, ISingletonDependency
9+
{
10+
protected AbpBackgroundJobOptions Options { get; }
11+
12+
public DynamicBackgroundJobHandlerProvider(IOptions<AbpBackgroundJobOptions> options)
13+
{
14+
Options = options.Value;
15+
}
16+
17+
public virtual void Register(string jobName, Func<DynamicBackgroundJobContext, Task> handler)
18+
{
19+
Options.AddDynamicJob(jobName, handler);
20+
}
21+
22+
public virtual void Register(string jobName, Action<DynamicBackgroundJobContext> handler)
23+
{
24+
Options.AddDynamicJob(jobName, handler);
25+
}
26+
27+
public virtual bool Unregister(string jobName)
28+
{
29+
return Options.RemoveDynamicJob(jobName);
30+
}
31+
32+
public virtual bool IsRegistered(string jobName)
33+
{
34+
return Options.GetJobOrNull(jobName)?.IsDynamic == true;
35+
}
36+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace Volo.Abp.BackgroundJobs;
5+
6+
public interface IDynamicBackgroundJobHandlerProvider
7+
{
8+
void Register(string jobName, Func<DynamicBackgroundJobContext, Task> handler);
9+
10+
void Register(string jobName, Action<DynamicBackgroundJobContext> handler);
11+
12+
bool Unregister(string jobName);
13+
14+
bool IsRegistered(string jobName);
15+
}

framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/JobExecutionContext.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Threading;
33
using Volo.Abp.DependencyInjection;
44

@@ -14,15 +14,19 @@ public class JobExecutionContext : IServiceProviderAccessor
1414

1515
public CancellationToken CancellationToken { get; }
1616

17+
public string? JobName { get; }
18+
1719
public JobExecutionContext(
1820
IServiceProvider serviceProvider,
1921
Type jobType,
2022
object jobArgs,
21-
CancellationToken cancellationToken = default)
23+
CancellationToken cancellationToken = default,
24+
string? jobName = null)
2225
{
2326
ServiceProvider = serviceProvider;
2427
JobType = jobType;
2528
JobArgs = jobArgs;
2629
CancellationToken = cancellationToken;
30+
JobName = jobName;
2731
}
2832
}

framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireJobExecutionAdapter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ public async Task ExecuteAsync(string queue, TArgs args, CancellationToken cance
3939

4040
using (var scope = ServiceScopeFactory.CreateScope())
4141
{
42-
var jobType = Options.GetJob(typeof(TArgs)).JobType;
43-
var context = new JobExecutionContext(scope.ServiceProvider, jobType, args!, cancellationToken: cancellationToken);
42+
var jobConfiguration = Options.GetJob(typeof(TArgs));
43+
var context = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType!, args!, cancellationToken: cancellationToken, jobName: jobConfiguration.JobName);
4444
await JobExecuter.ExecuteAsync(context);
4545
}
4646
}
@@ -83,7 +83,7 @@ public async Task ExecuteAsync(string queue, string jobName, string serializedAr
8383
{
8484
var jobConfiguration = Options.GetJob(jobName);
8585
var args = JsonSerializer.Deserialize(jobConfiguration.ArgsType, serializedArgs);
86-
var context = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType, args, cancellationToken: cancellationToken);
86+
var context = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType ?? typeof(object), args, cancellationToken: cancellationToken, jobName: jobName);
8787
await JobExecuter.ExecuteAsync(context);
8888
}
8989
}

framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzJobExecutionAdapter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ public async Task Execute(IJobExecutionContext context)
3939
using (var scope = ServiceScopeFactory.CreateScope())
4040
{
4141
var args = JsonSerializer.Deserialize<TArgs>(context.JobDetail.JobDataMap.GetString(nameof(TArgs))!);
42-
var jobType = Options.GetJob(typeof(TArgs)).JobType;
43-
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobType, args!, cancellationToken: context.CancellationToken);
42+
var jobConfiguration = Options.GetJob(typeof(TArgs));
43+
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType!, args!, cancellationToken: context.CancellationToken, jobName: jobConfiguration.JobName);
4444
try
4545
{
4646
await JobExecuter.ExecuteAsync(jobContext);
@@ -97,7 +97,7 @@ public async Task Execute(IJobExecutionContext context)
9797
var serializedArgs = context.JobDetail.JobDataMap.GetString(JobArgsKey)!;
9898
var jobConfiguration = Options.GetJob(jobName);
9999
var args = JsonSerializer.Deserialize(jobConfiguration.ArgsType, serializedArgs);
100-
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType, args, cancellationToken: context.CancellationToken);
100+
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType ?? typeof(object), args, cancellationToken: context.CancellationToken, jobName: jobName);
101101
try
102102
{
103103
await JobExecuter.ExecuteAsync(jobContext);

0 commit comments

Comments
 (0)