Browse Source

Refactor background job execution and management

pull/25059/head
maliming 7 months ago
parent
commit
70c333ad97
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 2
      docs/en/framework/infrastructure/background-jobs/hangfire.md
  2. 147
      docs/en/framework/infrastructure/background-jobs/index.md
  3. 39
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AbpBackgroundJobOptions.cs
  4. 12
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AnonymousJobExecutorAsyncBackgroundJob.cs
  5. 62
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AnonymousJobHandlerRegistry.cs
  6. 16
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/BackgroundJobExecuter.cs
  7. 112
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DefaultDynamicBackgroundJobManager.cs
  8. 4
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobExecutionContext.cs
  9. 9
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandler.cs
  10. 40
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandlerRegistry.cs
  11. 17
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IAnonymousJobHandlerRegistry.cs
  12. 15
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IBackgroundJobManager.cs
  13. 12
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobHandlerRegistry.cs
  14. 51
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobManager.cs
  15. 6
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/NullBackgroundJobManager.cs
  16. 53
      framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/AbpDashboardOptionsProvider.cs
  17. 62
      framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireBackgroundJobManager.cs
  18. 48
      framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireJobExecutionAdapter.cs
  19. 1
      framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/AbpBackgroundJobsQuartzModule.cs
  20. 59
      framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzBackgroundJobManager.cs
  21. 61
      framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzJobExecutionAdapter.cs
  22. 11
      framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/IJobQueue.cs
  23. 2
      framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/IJobQueueManager.cs
  24. 14
      framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs
  25. 27
      framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueueManager.cs
  26. 55
      framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/RabbitMqBackgroundJobManager.cs
  27. 4
      framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs
  28. 68
      framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpTickerQBackgroundJobManager.cs
  29. 33
      framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/DefaultBackgroundJobManager.cs
  30. 16
      framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobsTestModule.cs
  31. 51
      framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobExecuter_Tests.cs
  32. 38
      framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs
  33. 29
      modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs
  34. 47
      modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Jobs/SampleJobCreator.cs

2
docs/en/framework/infrastructure/background-jobs/hangfire.md

@ -149,8 +149,6 @@ namespace MyProject
Hangfire Dashboard provides information about your background jobs, including method names and serialized arguments as well as gives you an opportunity to manage them by performing different actions – retry, delete, trigger, etc. So it is important to restrict access to the Dashboard.
To make it secure by default, only local requests are allowed, however you can change this by following the [official documentation](http://docs.hangfire.io/en/latest/configuration/using-dashboard.html) of Hangfire.
When you enqueue jobs via anonymous handlers (`IBackgroundJobManager.EnqueueAsync(string jobName, object args)` + `IAnonymousJobHandlerRegistry`), ABP uses an internal transport job name (`Abp.AnonymousJob`), but the Hangfire dashboard display name tries to show the effective job name from payload (for example, `ProcessOrder`).
You can integrate the Hangfire dashboard to [ABP authorization system](../../fundamentals/authorization/index.md) using the **AbpHangfireAuthorizationFilter**
class. This class is defined in the `Volo.Abp.Hangfire` package. The following example, checks if the current user is logged in to the application:

147
docs/en/framework/infrastructure/background-jobs/index.md

@ -197,76 +197,6 @@ Enqueue method gets some optional arguments to control the background job:
* **priority** is used to control priority of the job item. It gets an `BackgroundJobPriority` enum which has `Low`, `BelowNormal`, `Normal` (default), `AboveNormal` and `Hight` fields.
* **delay** is used to wait a while (`TimeSpan`) before first try.
### Enqueue by ABP Job Name
You can also enqueue jobs by their ABP background job name at runtime:
```csharp
var jobName = BackgroundJobNameAttribute.GetName<EmailSendingArgs>(); // "emails" if configured via [BackgroundJobName]
await _backgroundJobManager.EnqueueAsync(
jobName,
new
{
EmailAddress = "user@abp.io",
Subject = "Welcome",
Body = "..."
}
);
```
`jobName` here is the ABP background job name (configuration key), not a provider queue name (like a Hangfire queue or RabbitMQ queue).
In this case, ABP resolves the target job configuration by `jobName` and serializes the `args` object.
If the `args` runtime type does not match the configured argument type, ABP normalizes the payload by serializing and deserializing it to the expected argument type.
### Anonymous Job Handlers
ABP supports registering runtime-resolved anonymous handlers keyed by a job name.
You can register handlers at startup:
```csharp
Configure<AbpBackgroundJobOptions>(options =>
{
options.AddAnonymousJobHandler("ProcessOrder", (context, cancellationToken) =>
{
// Parse or deserialize context.JsonData and run your logic.
return Task.CompletedTask;
});
});
```
You can also register/unregister handlers at runtime:
```csharp
public class MyService : ITransientDependency
{
private readonly IAnonymousJobHandlerRegistry _anonymousJobHandlerRegistry;
public MyService(IAnonymousJobHandlerRegistry anonymousJobHandlerRegistry)
{
_anonymousJobHandlerRegistry = anonymousJobHandlerRegistry;
}
public void Register()
{
_anonymousJobHandlerRegistry.Register("ProcessOrder", (context, cancellationToken) =>
{
return Task.CompletedTask;
});
}
}
```
Then enqueue it by name:
```csharp
await _backgroundJobManager.EnqueueAsync("ProcessOrder", new { OrderId = "42" });
```
ABP keeps a stable internal transport job name (`Abp.AnonymousJob`) for provider compatibility, while preserving your effective job name (`ProcessOrder`) in the payload.
### Disable Job Execution
You may want to disable background job execution for your application. This is generally needed if you want to execute background jobs in another process and disable it for the current process.
@ -416,6 +346,83 @@ If you don't want to use a distributed lock provider, you may go with the follow
* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances except one of them, so only the single instance executes the jobs (while other application instances can still queue jobs).
* Stop the background job manager (set `AbpBackgroundJobOptions.IsJobExecutionEnabled` to `false` as explained in the *Disable Job Execution* section) in all application instances and create a dedicated application (maybe a console application running in its own container or a Windows Service running in the background) to execute all the background jobs. This can be a good option if your background jobs consume high system resources (CPU, RAM or Disk), so you can deploy that background application to a dedicated server and your background jobs don't affect your application's performance.
## Dynamic Background Jobs
ABP provides `IDynamicBackgroundJobManager` for scenarios where you need to enqueue jobs by name at runtime, without requiring a strongly-typed job args class at compile time. This is useful for plugin systems, dynamic workflows, or any case where job types are not known ahead of time.
### Enqueue by Job Name (Typed Job)
If a typed job is already registered (e.g., via `[BackgroundJobName("emails")]`), you can enqueue it by name:
````csharp
public class MyService : ApplicationService
{
private readonly IDynamicBackgroundJobManager _dynamicJobManager;
public MyService(IDynamicBackgroundJobManager dynamicJobManager)
{
_dynamicJobManager = dynamicJobManager;
}
public async Task DoSomethingAsync()
{
await _dynamicJobManager.EnqueueAsync("emails", new
{
EmailAddress = "user@abp.io",
Subject = "Hello",
Body = "World"
});
}
}
````
The `IDynamicBackgroundJobManager` will look up the typed job configuration, deserialize the args to the expected type, and enqueue through the standard typed pipeline.
### Dynamic Job Handlers
You can also register dynamic handlers at runtime for jobs that don't have a pre-defined typed job class:
````csharp
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var dynamicJobManager = context.ServiceProvider
.GetRequiredService<IDynamicBackgroundJobManager>();
dynamicJobManager.RegisterHandler("ProcessOrder", async (context, ct) =>
{
var json = context.JsonData;
var serviceProvider = context.ServiceProvider;
// Process the order using JsonData and resolved services...
});
}
````
Then enqueue jobs using the registered name:
````csharp
await _dynamicJobManager.EnqueueAsync("ProcessOrder", new
{
OrderId = "ORD-001",
Amount = 99.99
});
````
### Handler Management
````csharp
// Check if a handler is registered
bool exists = _dynamicJobManager.IsHandlerRegistered("ProcessOrder");
// Unregister a handler
bool removed = _dynamicJobManager.UnregisterHandler("ProcessOrder");
````
### How It Works
- **Typed job path**: When the job name matches a registered typed job configuration, the args are serialized to JSON and deserialized to the expected args type, then enqueued through `IBackgroundJobManager.EnqueueAsync<TArgs>`.
- **Dynamic handler path**: When the job name matches a registered dynamic handler, the args are wrapped as `AnonymousJobArgs` (an internal transport type) and enqueued through `IBackgroundJobManager.EnqueueAsync<AnonymousJobArgs>`. When the job executes, the framework looks up the handler by name and invokes it.
- All dynamic jobs go through the **standard typed job pipeline**, which means they work with all providers (Default, Hangfire, Quartz, RabbitMQ, TickerQ) without any provider-specific changes.
## Integrations
Background job system is extensible and you can change the default background job manager with your own implementation or on of the pre-built integrations.

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

@ -1,17 +1,13 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundJobs;
public class AbpBackgroundJobOptions
{
private readonly Dictionary<Type, BackgroundJobConfiguration> _jobConfigurationsByArgsType;
private readonly ConcurrentDictionary<string, BackgroundJobConfiguration> _jobConfigurationsByName;
private readonly ConcurrentDictionary<string, Func<AnonymousJobExecutionContext, CancellationToken, Task>> _anonymousHandlers = new();
private readonly Dictionary<string, BackgroundJobConfiguration> _jobConfigurationsByName;
/// <summary>
/// Default: true.
@ -27,7 +23,7 @@ public class AbpBackgroundJobOptions
public AbpBackgroundJobOptions()
{
_jobConfigurationsByArgsType = new Dictionary<Type, BackgroundJobConfiguration>();
_jobConfigurationsByName = new ConcurrentDictionary<string, BackgroundJobConfiguration>();
_jobConfigurationsByName = new Dictionary<string, BackgroundJobConfiguration>();
GetBackgroundJobName = BackgroundJobNameAttribute.GetName;
}
@ -62,7 +58,7 @@ public class AbpBackgroundJobOptions
public BackgroundJobConfiguration? GetJobOrNull(string name)
{
return _jobConfigurationsByName.TryGetValue(name, out var config) ? config : null;
return _jobConfigurationsByName.GetOrDefault(name);
}
public IReadOnlyList<BackgroundJobConfiguration> GetJobs()
@ -85,33 +81,4 @@ public class AbpBackgroundJobOptions
_jobConfigurationsByArgsType[jobConfiguration.ArgsType] = jobConfiguration;
_jobConfigurationsByName[jobConfiguration.JobName] = jobConfiguration;
}
public void AddAnonymousJobHandler(string jobName, Func<AnonymousJobExecutionContext, CancellationToken, Task> handler)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
Check.NotNull(handler, nameof(handler));
_anonymousHandlers[jobName] = handler;
}
public void AddAnonymousJobHandler(string jobName, Action<AnonymousJobExecutionContext, CancellationToken> handler)
{
Check.NotNull(handler, nameof(handler));
AddAnonymousJobHandler(jobName, (context, ct) =>
{
handler(context, ct);
return Task.CompletedTask;
});
}
internal bool TryGetAnonymousHandler(string jobName, out Func<AnonymousJobExecutionContext, CancellationToken, Task>? handler)
{
return _anonymousHandlers.TryGetValue(jobName, out handler);
}
internal bool IsAnonymousJobRegistered(string jobName)
{
return _anonymousHandlers.ContainsKey(jobName);
}
}

12
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AnonymousJobExecutorAsyncBackgroundJob.cs

@ -9,11 +9,11 @@ namespace Volo.Abp.BackgroundJobs;
public class AnonymousJobExecutorAsyncBackgroundJob : AsyncBackgroundJob<AnonymousJobArgs>, ITransientDependency
{
protected IAnonymousJobHandlerRegistry HandlerRegistry { get; }
protected IDynamicBackgroundJobHandlerRegistry HandlerRegistry { get; }
protected IServiceProvider ServiceProvider { get; }
public AnonymousJobExecutorAsyncBackgroundJob(
IAnonymousJobHandlerRegistry handlerRegistry,
IDynamicBackgroundJobHandlerRegistry handlerRegistry,
IServiceProvider serviceProvider)
{
HandlerRegistry = handlerRegistry;
@ -23,7 +23,7 @@ public class AnonymousJobExecutorAsyncBackgroundJob : AsyncBackgroundJob<Anonymo
public override async Task ExecuteAsync(AnonymousJobArgs args)
{
Logger.LogInformation(
"Executing anonymous transport job. TransportJobName: {TransportJobName}, EffectiveJobName: {EffectiveJobName}",
"Executing dynamic job. TransportJobName: {TransportJobName}, EffectiveJobName: {EffectiveJobName}",
AnonymousJobArgs.JobNameConstant,
args.JobName
);
@ -31,11 +31,13 @@ public class AnonymousJobExecutorAsyncBackgroundJob : AsyncBackgroundJob<Anonymo
var handler = HandlerRegistry.Get(args.JobName);
if (handler == null)
{
throw new AbpException("No anonymous job handler registered for: " + args.JobName);
throw new AbpException(
$"No dynamic job handler registered for: {args.JobName}. " +
$"The handler may have been unregistered or the application restarted since the job was enqueued.");
}
var cancellationToken = ServiceProvider.GetRequiredService<ICancellationTokenProvider>().Token;
var executionContext = new AnonymousJobExecutionContext(args.JobName, args.JsonData, ServiceProvider);
var executionContext = new DynamicBackgroundJobExecutionContext(args.JobName, args.JsonData, ServiceProvider);
await handler(executionContext, cancellationToken);
}
}

62
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AnonymousJobHandlerRegistry.cs

@ -1,62 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BackgroundJobs;
public class AnonymousJobHandlerRegistry : IAnonymousJobHandlerRegistry, ISingletonDependency
{
private readonly ConcurrentDictionary<string, Func<AnonymousJobExecutionContext, CancellationToken, Task>> _handlers = new();
private readonly AbpBackgroundJobOptions _options;
public AnonymousJobHandlerRegistry(IOptions<AbpBackgroundJobOptions> options)
{
_options = options.Value;
}
public virtual void Register(string jobName, Func<AnonymousJobExecutionContext, CancellationToken, Task> handler)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
Check.NotNull(handler, nameof(handler));
_handlers[jobName] = handler;
}
public virtual void Register(string jobName, Action<AnonymousJobExecutionContext, CancellationToken> handler)
{
Check.NotNull(handler, nameof(handler));
Register(jobName, (context, ct) =>
{
handler(context, ct);
return Task.CompletedTask;
});
}
public virtual bool Unregister(string jobName)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
return _handlers.TryRemove(jobName, out _);
}
public virtual bool IsRegistered(string jobName)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
return _handlers.ContainsKey(jobName) || _options.IsAnonymousJobRegistered(jobName);
}
public virtual Func<AnonymousJobExecutionContext, CancellationToken, Task>? Get(string jobName)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
if (_handlers.TryGetValue(jobName, out var handler))
{
return handler;
}
return _options.TryGetAnonymousHandler(jobName, out handler) ? handler : null;
}
}

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

@ -16,7 +16,7 @@ public class BackgroundJobExecuter : IBackgroundJobExecuter, ITransientDependenc
public ILogger<BackgroundJobExecuter> Logger { protected get; set; }
protected AbpBackgroundJobOptions Options { get; }
protected ICurrentTenant CurrentTenant { get; }
public BackgroundJobExecuter(IOptions<AbpBackgroundJobOptions> options, ICurrentTenant currentTenant)
@ -28,11 +28,6 @@ public class BackgroundJobExecuter : IBackgroundJobExecuter, ITransientDependenc
}
public virtual async Task ExecuteAsync(JobExecutionContext context)
{
await ExecuteTypedHandlerAsync(context);
}
protected virtual async Task ExecuteTypedHandlerAsync(JobExecutionContext context)
{
var job = context.ServiceProvider.GetService(context.JobType);
if (job == null)
@ -50,7 +45,7 @@ public class BackgroundJobExecuter : IBackgroundJobExecuter, ITransientDependenc
try
{
using (CurrentTenant.Change(GetJobArgsTenantId(context.JobArgs)))
using(CurrentTenant.Change(GetJobArgsTenantId(context.JobArgs)))
{
var cancellationTokenProvider =
context.ServiceProvider.GetRequiredService<ICancellationTokenProvider>();
@ -59,14 +54,15 @@ public class BackgroundJobExecuter : IBackgroundJobExecuter, ITransientDependenc
{
if (jobExecuteMethod.Name == nameof(IAsyncBackgroundJob<object>.ExecuteAsync))
{
await ((Task)jobExecuteMethod.Invoke(job, [context.JobArgs])!);
await ((Task)jobExecuteMethod.Invoke(job, new[] { context.JobArgs })!);
}
else
{
jobExecuteMethod.Invoke(job, [context.JobArgs]);
jobExecuteMethod.Invoke(job, new[] { context.JobArgs });
}
}
}
}
catch (Exception ex)
{
@ -83,7 +79,7 @@ public class BackgroundJobExecuter : IBackgroundJobExecuter, ITransientDependenc
};
}
}
protected virtual Guid? GetJobArgsTenantId(object jobArgs)
{
return jobArgs switch

112
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DefaultDynamicBackgroundJobManager.cs

@ -0,0 +1,112 @@
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Json;
namespace Volo.Abp.BackgroundJobs;
public class DefaultDynamicBackgroundJobManager : IDynamicBackgroundJobManager, ITransientDependency
{
private static readonly ConcurrentDictionary<Type, MethodInfo> EnqueueMethodCache = new();
protected IBackgroundJobManager BackgroundJobManager { get; }
protected IDynamicBackgroundJobHandlerRegistry HandlerRegistry { get; }
protected AbpBackgroundJobOptions BackgroundJobOptions { get; }
protected IJsonSerializer JsonSerializer { get; }
public DefaultDynamicBackgroundJobManager(
IBackgroundJobManager backgroundJobManager,
IDynamicBackgroundJobHandlerRegistry handlerRegistry,
IOptions<AbpBackgroundJobOptions> backgroundJobOptions,
IJsonSerializer jsonSerializer)
{
BackgroundJobManager = backgroundJobManager;
HandlerRegistry = handlerRegistry;
BackgroundJobOptions = backgroundJobOptions.Value;
JsonSerializer = jsonSerializer;
}
public virtual async Task<string> EnqueueAsync(
string jobName,
object args,
BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
Check.NotNull(args, nameof(args));
var jobConfiguration = BackgroundJobOptions.GetJobOrNull(jobName);
if (jobConfiguration != null)
{
return await EnqueueTypedJobAsync(jobConfiguration, args, priority, delay);
}
if (HandlerRegistry.IsRegistered(jobName))
{
return await EnqueueAnonymousJobAsync(jobName, args, priority, delay);
}
throw new AbpException(
$"No typed job configuration or dynamic handler registered for job name: {jobName}");
}
public virtual void RegisterHandler(
string jobName,
DynamicBackgroundJobHandler handler)
{
HandlerRegistry.Register(jobName, handler);
}
public virtual bool UnregisterHandler(string jobName)
{
return HandlerRegistry.Unregister(jobName);
}
public virtual bool IsHandlerRegistered(string jobName)
{
return HandlerRegistry.IsRegistered(jobName);
}
protected virtual async Task<string> EnqueueTypedJobAsync(
BackgroundJobConfiguration jobConfiguration,
object args,
BackgroundJobPriority priority,
TimeSpan? delay)
{
var argsType = jobConfiguration.ArgsType;
// Normalize args to the expected type via JSON round-trip
var json = JsonSerializer.Serialize(args);
var typedArgs = JsonSerializer.Deserialize(argsType, json);
var enqueueMethod = GetOrCreateEnqueueMethod(argsType);
var task = (Task<string>)enqueueMethod.Invoke(BackgroundJobManager, [typedArgs, priority, delay])!;
return await task;
}
protected virtual Task<string> EnqueueAnonymousJobAsync(
string jobName,
object args,
BackgroundJobPriority priority,
TimeSpan? delay)
{
var jsonData = JsonSerializer.Serialize(args);
var anonymousArgs = new AnonymousJobArgs(jobName, jsonData);
return BackgroundJobManager.EnqueueAsync(anonymousArgs, priority, delay);
}
private static MethodInfo GetOrCreateEnqueueMethod(Type argsType)
{
return EnqueueMethodCache.GetOrAdd(argsType, static type =>
{
var method = typeof(IBackgroundJobManager).GetMethod(
nameof(IBackgroundJobManager.EnqueueAsync),
BindingFlags.Public | BindingFlags.Instance);
return method!.MakeGenericMethod(type);
});
}
}

4
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/AnonymousJobExecutionContext.cs → framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobExecutionContext.cs

@ -2,7 +2,7 @@ using System;
namespace Volo.Abp.BackgroundJobs;
public class AnonymousJobExecutionContext
public class DynamicBackgroundJobExecutionContext
{
public string JobName { get; }
@ -10,7 +10,7 @@ public class AnonymousJobExecutionContext
public IServiceProvider ServiceProvider { get; }
public AnonymousJobExecutionContext(
public DynamicBackgroundJobExecutionContext(
string jobName,
string jsonData,
IServiceProvider serviceProvider)

9
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandler.cs

@ -0,0 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundJobs;
/// <summary>
/// Represents a handler delegate for dynamic background jobs.
/// </summary>
public delegate Task DynamicBackgroundJobHandler(DynamicBackgroundJobExecutionContext context, CancellationToken cancellationToken);

40
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/DynamicBackgroundJobHandlerRegistry.cs

@ -0,0 +1,40 @@
using System.Collections.Concurrent;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BackgroundJobs;
public class DynamicBackgroundJobHandlerRegistry : IDynamicBackgroundJobHandlerRegistry, ISingletonDependency
{
protected ConcurrentDictionary<string, DynamicBackgroundJobHandler> Handlers { get; }
public DynamicBackgroundJobHandlerRegistry()
{
Handlers = new ConcurrentDictionary<string, DynamicBackgroundJobHandler>();
}
public virtual void Register(string jobName, DynamicBackgroundJobHandler handler)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
Check.NotNull(handler, nameof(handler));
Handlers[jobName] = handler;
}
public virtual bool Unregister(string jobName)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
return Handlers.TryRemove(jobName, out _);
}
public virtual bool IsRegistered(string jobName)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
return Handlers.ContainsKey(jobName);
}
public virtual DynamicBackgroundJobHandler? Get(string jobName)
{
Check.NotNullOrWhiteSpace(jobName, nameof(jobName));
return Handlers.TryGetValue(jobName, out var handler) ? handler : null;
}
}

17
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IAnonymousJobHandlerRegistry.cs

@ -1,17 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundJobs;
public interface IAnonymousJobHandlerRegistry
{
void Register(string jobName, Func<AnonymousJobExecutionContext, System.Threading.CancellationToken, Task> handler);
void Register(string jobName, Action<AnonymousJobExecutionContext, System.Threading.CancellationToken> handler);
bool Unregister(string jobName);
bool IsRegistered(string jobName);
Func<AnonymousJobExecutionContext, System.Threading.CancellationToken, Task>? Get(string jobName);
}

15
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IBackgroundJobManager.cs

@ -21,19 +21,4 @@ public interface IBackgroundJobManager
BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null
);
/// <summary>
/// Enqueues a job to be executed by its name.
/// </summary>
/// <param name="jobName">Name of the background job.</param>
/// <param name="args">Job arguments.</param>
/// <param name="priority">Job priority.</param>
/// <param name="delay">Job delay (wait duration before first try).</param>
/// <returns>Unique identifier of a background job.</returns>
Task<string> EnqueueAsync(
string jobName,
object args,
BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null
);
}

12
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobHandlerRegistry.cs

@ -0,0 +1,12 @@
namespace Volo.Abp.BackgroundJobs;
public interface IDynamicBackgroundJobHandlerRegistry
{
void Register(string jobName, DynamicBackgroundJobHandler handler);
bool Unregister(string jobName);
bool IsRegistered(string jobName);
DynamicBackgroundJobHandler? Get(string jobName);
}

51
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IDynamicBackgroundJobManager.cs

@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundJobs;
/// <summary>
/// Manages dynamic background jobs that can be enqueued by name
/// without requiring a strongly-typed job args class at compile time.
/// Also supports registering dynamic job handlers at runtime.
/// </summary>
public interface IDynamicBackgroundJobManager
{
/// <summary>
/// Enqueues a job by name with dynamic payload.
/// If a typed job configuration exists for this name, the args will be
/// deserialized to the configured args type and enqueued through the typed pipeline.
/// If a dynamic handler is registered, the args will be wrapped as
/// <see cref="AnonymousJobArgs"/> and enqueued through the standard typed pipeline.
/// </summary>
/// <param name="jobName">Name of the background job.</param>
/// <param name="args">Job arguments (will be serialized to JSON).</param>
/// <param name="priority">Job priority.</param>
/// <param name="delay">Job delay (wait duration before first try).</param>
/// <returns>Unique identifier of a background job.</returns>
Task<string> EnqueueAsync(
string jobName,
object args,
BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null);
/// <summary>
/// Registers a dynamic job handler at runtime.
/// </summary>
/// <param name="jobName">Unique name for the dynamic job.</param>
/// <param name="handler">The handler delegate to execute when the job runs.</param>
void RegisterHandler(string jobName, DynamicBackgroundJobHandler handler);
/// <summary>
/// Unregisters a previously registered dynamic job handler.
/// </summary>
/// <param name="jobName">Name of the dynamic job to unregister.</param>
/// <returns>True if the handler was found and removed; false otherwise.</returns>
bool UnregisterHandler(string jobName);
/// <summary>
/// Checks whether a dynamic handler is registered for the given job name.
/// </summary>
/// <param name="jobName">Name of the dynamic job.</param>
/// <returns>True if registered; false otherwise.</returns>
bool IsHandlerRegistered(string jobName);
}

6
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/NullBackgroundJobManager.cs

@ -21,10 +21,4 @@ public class NullBackgroundJobManager : IBackgroundJobManager, ISingletonDepende
{
throw new AbpException("Background job system has not a real implementation. If it's mandatory, use an implementation (either the default provider or a 3rd party implementation). If it's optional, check IBackgroundJobManager.IsAvailable() extension method and act based on it.");
}
public virtual Task<string> EnqueueAsync(string jobName, object args, BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null)
{
throw new AbpException("Background job system has not a real implementation. If it's mandatory, use an implementation (either the default provider or a 3rd party implementation). If it's optional, check IBackgroundJobManager.IsAvailable() extension method and act based on it.");
}
}

53
framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/AbpDashboardOptionsProvider.cs

@ -1,5 +1,4 @@
using System.Linq;
using System.Text.Json;
using System.Threading;
using Hangfire;
using Microsoft.Extensions.Options;
@ -23,65 +22,13 @@ public class AbpDashboardOptionsProvider : ITransientDependency
DisplayNameFunc = (_, job) =>
{
var jobName = job.ToString();
if (job.Args.Count == 3 && job.Args.Last() is CancellationToken)
{
if (job.Args[1] is AnonymousJobArgs anonymousJobArgs)
{
return anonymousJobArgs.JobName;
}
jobName = AbpBackgroundJobOptions.GetJob(job.Args[1].GetType()).JobName;
}
if (job.Args.Count == 4 && job.Args.Last() is CancellationToken)
{
if (job.Args[1] is string transportJobName)
{
if (transportJobName == AnonymousJobArgs.JobNameConstant &&
job.Args[2] is string serializedArgs &&
TryGetEffectiveJobName(serializedArgs, out var effectiveJobName))
{
return effectiveJobName;
}
return transportJobName;
}
}
return jobName;
}
};
}
protected virtual bool TryGetEffectiveJobName(string serializedArgs, out string effectiveJobName)
{
effectiveJobName = string.Empty;
try
{
using var document = JsonDocument.Parse(serializedArgs);
if (TryGetJobNameElement(document.RootElement, out var jobNameElement))
{
var jobName = jobNameElement.GetString();
if (!string.IsNullOrWhiteSpace(jobName))
{
effectiveJobName = jobName!;
return true;
}
}
}
catch
{
// Ignore parse errors and fallback to transport job name.
}
return false;
}
protected virtual bool TryGetJobNameElement(JsonElement rootElement, out JsonElement jobNameElement)
{
return rootElement.TryGetProperty(nameof(AnonymousJobArgs.JobName), out jobNameElement)
|| rootElement.TryGetProperty("jobName", out jobNameElement);
}
}

62
framework/src/Volo.Abp.BackgroundJobs.HangFire/Volo/Abp/BackgroundJobs/Hangfire/HangfireBackgroundJobManager.cs

@ -3,12 +3,9 @@ using System.Reflection;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.States;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Hangfire;
using Volo.Abp.Json;
namespace Volo.Abp.BackgroundJobs.Hangfire;
@ -17,21 +14,11 @@ public class HangfireBackgroundJobManager : IBackgroundJobManager, ITransientDep
{
protected IOptions<AbpBackgroundJobOptions> BackgroundJobOptions { get; }
protected IOptions<AbpHangfireOptions> HangfireOptions { get; }
protected IJsonSerializer JsonSerializer { get; }
protected IAnonymousJobHandlerRegistry AnonymousJobHandlerRegistry { get; }
public ILogger<HangfireBackgroundJobManager> Logger { get; set; }
public HangfireBackgroundJobManager(
IOptions<AbpBackgroundJobOptions> backgroundJobOptions,
IOptions<AbpHangfireOptions> hangfireOptions,
IJsonSerializer jsonSerializer,
IAnonymousJobHandlerRegistry anonymousJobHandlerRegistry)
public HangfireBackgroundJobManager(IOptions<AbpBackgroundJobOptions> backgroundJobOptions, IOptions<AbpHangfireOptions> hangfireOptions)
{
BackgroundJobOptions = backgroundJobOptions;
HangfireOptions = hangfireOptions;
JsonSerializer = jsonSerializer;
AnonymousJobHandlerRegistry = anonymousJobHandlerRegistry;
Logger = NullLogger<HangfireBackgroundJobManager>.Instance;
}
public virtual Task<string> EnqueueAsync<TArgs>(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal,
@ -47,54 +34,9 @@ public class HangfireBackgroundJobManager : IBackgroundJobManager, ITransientDep
));
}
public virtual Task<string> EnqueueAsync(string jobName, object args, BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null)
{
if (ShouldWrapAsAnonymousJob(jobName))
{
Logger.LogInformation(
"Wrapping job into anonymous transport. TransportJobName: {TransportJobName}, EffectiveJobName: {EffectiveJobName}",
AnonymousJobArgs.JobNameConstant,
jobName
);
var jsonData = JsonSerializer.Serialize(args);
var anonymousArgs = new AnonymousJobArgs(jobName, jsonData);
return EnqueueAsync(AnonymousJobArgs.JobNameConstant, anonymousArgs, priority, delay);
}
var serializedArgs = JsonSerializer.Serialize(args);
var queueName = GetQueueName(jobName);
return Task.FromResult(delay.HasValue
? BackgroundJob.Schedule<HangfireJobExecutionAdapter>(
adapter => adapter.ExecuteAsync(queueName, jobName, serializedArgs, default),
delay.Value
)
: BackgroundJob.Enqueue<HangfireJobExecutionAdapter>(
adapter => adapter.ExecuteAsync(queueName, jobName, serializedArgs, default)
));
}
protected virtual bool ShouldWrapAsAnonymousJob(string jobName)
{
return jobName != AnonymousJobArgs.JobNameConstant &&
AnonymousJobHandlerRegistry.IsRegistered(jobName) &&
BackgroundJobOptions.Value.GetJobOrNull(jobName) == null;
}
protected virtual string GetQueueName(Type argsType)
{
return GetQueueName(BackgroundJobOptions.Value.GetJob(argsType));
}
protected virtual string GetQueueName(string jobName)
{
return GetQueueName(BackgroundJobOptions.Value.GetJob(jobName));
}
protected virtual string GetQueueName(BackgroundJobConfiguration jobConfiguration)
{
var queueAttribute = jobConfiguration.JobType.GetCustomAttribute<QueueAttribute>();
var queueAttribute = BackgroundJobOptions.Value.GetJob(argsType).JobType.GetCustomAttribute<QueueAttribute>();
return queueAttribute != null ? HangfireOptions.Value.DefaultQueuePrefix + queueAttribute.Queue : HangfireOptions.Value.DefaultQueue;
}
}

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

@ -3,7 +3,6 @@ using System.Threading.Tasks;
using Hangfire;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Json;
namespace Volo.Abp.BackgroundJobs.Hangfire;
@ -39,51 +38,8 @@ public class HangfireJobExecutionAdapter<TArgs>
using (var scope = ServiceScopeFactory.CreateScope())
{
var jobConfiguration = Options.GetJob(typeof(TArgs));
var context = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType!, args!, cancellationToken: cancellationToken);
await JobExecuter.ExecuteAsync(context);
}
}
}
public class HangfireJobExecutionAdapter
{
protected AbpBackgroundJobOptions Options { get; }
protected IServiceScopeFactory ServiceScopeFactory { get; }
protected IBackgroundJobExecuter JobExecuter { get; }
protected IJsonSerializer JsonSerializer { get; }
public HangfireJobExecutionAdapter(
IOptions<AbpBackgroundJobOptions> options,
IBackgroundJobExecuter jobExecuter,
IServiceScopeFactory serviceScopeFactory,
IJsonSerializer jsonSerializer)
{
JobExecuter = jobExecuter;
ServiceScopeFactory = serviceScopeFactory;
JsonSerializer = jsonSerializer;
Options = options.Value;
}
[Queue("{0}")]
public async Task ExecuteAsync(string queue, string jobName, string serializedArgs, CancellationToken cancellationToken = default)
{
if (!Options.IsJobExecutionEnabled)
{
throw new AbpException(
"Background job execution is disabled. " +
"This method should not be called! " +
"If you want to enable the background job execution, " +
$"set {nameof(AbpBackgroundJobOptions)}.{nameof(AbpBackgroundJobOptions.IsJobExecutionEnabled)} to true! " +
"If you've intentionally disabled job execution and this seems a bug, please report it."
);
}
using (var scope = ServiceScopeFactory.CreateScope())
{
var jobConfiguration = Options.GetJob(jobName);
var args = JsonSerializer.Deserialize(jobConfiguration.ArgsType, serializedArgs);
var context = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType, args, cancellationToken: cancellationToken);
var jobType = Options.GetJob(typeof(TArgs)).JobType;
var context = new JobExecutionContext(scope.ServiceProvider, jobType, args!, cancellationToken: cancellationToken);
await JobExecuter.ExecuteAsync(context);
}
}

1
framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/AbpBackgroundJobsQuartzModule.cs

@ -15,7 +15,6 @@ public class AbpBackgroundJobsQuartzModule : AbpModule
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddTransient(typeof(QuartzJobExecutionAdapter<>));
context.Services.AddTransient<QuartzJobExecutionAdapter>();
}
public override void OnPreApplicationInitialization(ApplicationInitializationContext context)

59
framework/src/Volo.Abp.BackgroundJobs.Quartz/Volo/Abp/BackgroundJobs/Quartz/QuartzBackgroundJobManager.cs

@ -1,7 +1,5 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Quartz;
using Volo.Abp.DependencyInjection;
@ -18,26 +16,14 @@ public class QuartzBackgroundJobManager : IBackgroundJobManager, ITransientDepen
protected IScheduler Scheduler { get; }
protected AbpBackgroundJobQuartzOptions Options { get; }
protected AbpBackgroundJobOptions BackgroundJobOptions { get; }
protected IJsonSerializer JsonSerializer { get; }
protected IAnonymousJobHandlerRegistry AnonymousJobHandlerRegistry { get; }
public ILogger<QuartzBackgroundJobManager> Logger { get; set; }
public QuartzBackgroundJobManager(
IScheduler scheduler,
IOptions<AbpBackgroundJobQuartzOptions> options,
IOptions<AbpBackgroundJobOptions> backgroundJobOptions,
IJsonSerializer jsonSerializer,
IAnonymousJobHandlerRegistry anonymousJobHandlerRegistry)
public QuartzBackgroundJobManager(IScheduler scheduler, IOptions<AbpBackgroundJobQuartzOptions> options, IJsonSerializer jsonSerializer)
{
Scheduler = scheduler;
JsonSerializer = jsonSerializer;
Options = options.Value;
BackgroundJobOptions = backgroundJobOptions.Value;
AnonymousJobHandlerRegistry = anonymousJobHandlerRegistry;
Logger = NullLogger<QuartzBackgroundJobManager>.Instance;
}
public virtual async Task<string> EnqueueAsync<TArgs>(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal,
@ -46,31 +32,6 @@ public class QuartzBackgroundJobManager : IBackgroundJobManager, ITransientDepen
return await ReEnqueueAsync(args, Options.RetryCount, Options.RetryIntervalMillisecond, priority, delay);
}
public virtual async Task<string> EnqueueAsync(string jobName, object args, BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null)
{
if (ShouldWrapAsAnonymousJob(jobName))
{
Logger.LogInformation(
"Wrapping job into anonymous transport. TransportJobName: {TransportJobName}, EffectiveJobName: {EffectiveJobName}",
AnonymousJobArgs.JobNameConstant,
jobName
);
var jsonData = JsonSerializer.Serialize(args);
var anonymousArgs = new AnonymousJobArgs(jobName, jsonData);
return await EnqueueAsync(AnonymousJobArgs.JobNameConstant, anonymousArgs, priority, delay);
}
return await ReEnqueueAsync(jobName, args, Options.RetryCount, Options.RetryIntervalMillisecond, priority, delay);
}
protected virtual bool ShouldWrapAsAnonymousJob(string jobName)
{
return jobName != AnonymousJobArgs.JobNameConstant &&
AnonymousJobHandlerRegistry.IsRegistered(jobName) &&
BackgroundJobOptions.GetJobOrNull(jobName) == null;
}
public virtual async Task<string> ReEnqueueAsync<TArgs>(TArgs args, int retryCount, int retryIntervalMillisecond,
BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)
{
@ -87,22 +48,4 @@ public class QuartzBackgroundJobManager : IBackgroundJobManager, ITransientDepen
await Scheduler.ScheduleJob(jobDetail, trigger);
return jobDetail.Key.ToString();
}
public virtual async Task<string> ReEnqueueAsync(string jobName, object args, int retryCount, int retryIntervalMillisecond,
BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)
{
var jobDataMap = new JobDataMap
{
{QuartzJobExecutionAdapter.JobNameKey, jobName},
{QuartzJobExecutionAdapter.JobArgsKey, JsonSerializer.Serialize(args)},
{JobDataPrefix + nameof(Options.RetryCount), retryCount.ToString()},
{JobDataPrefix + nameof(Options.RetryIntervalMillisecond), retryIntervalMillisecond.ToString()},
{JobDataPrefix + RetryIndex, "0"}
};
var jobDetail = JobBuilder.Create<QuartzJobExecutionAdapter>().RequestRecovery().SetJobData(jobDataMap).Build();
var trigger = !delay.HasValue ? TriggerBuilder.Create().StartNow().Build() : TriggerBuilder.Create().StartAt(new DateTimeOffset(DateTime.Now.Add(delay.Value))).Build();
await Scheduler.ScheduleJob(jobDetail, trigger);
return jobDetail.Key.ToString();
}
}

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

@ -39,65 +39,8 @@ public class QuartzJobExecutionAdapter<TArgs> : IJob
using (var scope = ServiceScopeFactory.CreateScope())
{
var args = JsonSerializer.Deserialize<TArgs>(context.JobDetail.JobDataMap.GetString(nameof(TArgs))!);
var jobConfiguration = Options.GetJob(typeof(TArgs));
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType!, args!, context.CancellationToken);
try
{
await JobExecuter.ExecuteAsync(jobContext);
}
catch (Exception exception)
{
var jobExecutionException = new JobExecutionException(exception);
var retryIndex = context.JobDetail.JobDataMap.GetString(QuartzBackgroundJobManager.JobDataPrefix + QuartzBackgroundJobManager.RetryIndex)!.To<int>();
retryIndex++;
context.JobDetail.JobDataMap.Put(QuartzBackgroundJobManager.JobDataPrefix + QuartzBackgroundJobManager.RetryIndex, retryIndex.ToString());
await BackgroundJobQuartzOptions.RetryStrategy.Invoke(retryIndex, context, jobExecutionException);
throw jobExecutionException;
}
}
}
}
public class QuartzJobExecutionAdapter : IJob
{
public const string JobNameKey = "AbpJobName";
public const string JobArgsKey = "AbpJobArgs";
public ILogger<QuartzJobExecutionAdapter> Logger { get; set; }
protected AbpBackgroundJobOptions Options { get; }
protected AbpBackgroundJobQuartzOptions BackgroundJobQuartzOptions { get; }
protected IServiceScopeFactory ServiceScopeFactory { get; }
protected IBackgroundJobExecuter JobExecuter { get; }
protected IJsonSerializer JsonSerializer { get; }
public QuartzJobExecutionAdapter(
IOptions<AbpBackgroundJobOptions> options,
IOptions<AbpBackgroundJobQuartzOptions> backgroundJobQuartzOptions,
IBackgroundJobExecuter jobExecuter,
IServiceScopeFactory serviceScopeFactory,
IJsonSerializer jsonSerializer)
{
JobExecuter = jobExecuter;
ServiceScopeFactory = serviceScopeFactory;
JsonSerializer = jsonSerializer;
Options = options.Value;
BackgroundJobQuartzOptions = backgroundJobQuartzOptions.Value;
Logger = NullLogger<QuartzJobExecutionAdapter>.Instance;
}
public async Task Execute(IJobExecutionContext context)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var jobName = context.JobDetail.JobDataMap.GetString(JobNameKey)!;
var serializedArgs = context.JobDetail.JobDataMap.GetString(JobArgsKey)!;
var jobConfiguration = Options.GetJob(jobName);
var args = JsonSerializer.Deserialize(jobConfiguration.ArgsType, serializedArgs);
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType, args, cancellationToken: context.CancellationToken);
var jobType = Options.GetJob(typeof(TArgs)).JobType;
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobType, args!, cancellationToken: context.CancellationToken);
try
{
await JobExecuter.ExecuteAsync(jobContext);

11
framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/IJobQueue.cs

@ -4,16 +4,7 @@ using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundJobs.RabbitMQ;
public interface IJobQueue : IRunnable, IDisposable
{
Task<string?> EnqueueAsync(
object args,
BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null
);
}
public interface IJobQueue<in TArgs> : IJobQueue
public interface IJobQueue<in TArgs> : IRunnable, IDisposable
{
Task<string?> EnqueueAsync(
TArgs args,

2
framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/IJobQueueManager.cs

@ -6,6 +6,4 @@ namespace Volo.Abp.BackgroundJobs.RabbitMQ;
public interface IJobQueueManager : IRunnable
{
Task<IJobQueue<TArgs>> GetAsync<TArgs>();
Task<IJobQueue> GetAsync(string jobName);
}

14
framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueue.cs

@ -75,20 +75,6 @@ public class JobQueue<TArgs> : IJobQueue<TArgs>
);
}
async Task<string?> IJobQueue.EnqueueAsync(
object args,
BackgroundJobPriority priority,
TimeSpan? delay)
{
if (args is TArgs typedArgs)
{
return await EnqueueAsync(typedArgs, priority, delay);
}
var serializedArgs = Serializer.Serialize(args);
return await EnqueueAsync((TArgs)Serializer.Deserialize(serializedArgs, typeof(TArgs)), priority, delay);
}
public virtual async Task<string?> EnqueueAsync(
TArgs args,
BackgroundJobPriority priority = BackgroundJobPriority.Normal,

27
framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/JobQueueManager.cs

@ -80,31 +80,4 @@ public class JobQueueManager : IJobQueueManager, ISingletonDependency
return (IJobQueue<TArgs>)jobQueue;
}
}
public async Task<IJobQueue> GetAsync(string jobName)
{
if (JobQueues.TryGetValue(jobName, out var jobQueue))
{
return (IJobQueue)jobQueue;
}
using (await SyncSemaphore.LockAsync())
{
if (JobQueues.TryGetValue(jobName, out jobQueue))
{
return (IJobQueue)jobQueue;
}
var jobConfiguration = Options.GetJob(jobName);
jobQueue = (IRunnable)ServiceProvider
.GetRequiredService(typeof(IJobQueue<>).MakeGenericType(jobConfiguration.ArgsType));
await jobQueue.StartAsync();
JobQueues.TryAdd(jobName, jobQueue);
return (IJobQueue)jobQueue;
}
}
}

55
framework/src/Volo.Abp.BackgroundJobs.RabbitMQ/Volo/Abp/BackgroundJobs/RabbitMQ/RabbitMqBackgroundJobManager.cs

@ -1,70 +1,25 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Json;
namespace Volo.Abp.BackgroundJobs.RabbitMQ;
[Dependency(ReplaceServices = true)]
public class RabbitMqBackgroundJobManager : IBackgroundJobManager, ITransientDependency
{
protected IJobQueueManager JobQueueManager { get; }
protected IAnonymousJobHandlerRegistry AnonymousJobHandlerRegistry { get; }
protected AbpBackgroundJobOptions BackgroundJobOptions { get; }
protected IJsonSerializer JsonSerializer { get; }
public ILogger<RabbitMqBackgroundJobManager> Logger { get; set; }
private readonly IJobQueueManager _jobQueueManager;
public RabbitMqBackgroundJobManager(
IJobQueueManager jobQueueManager,
IAnonymousJobHandlerRegistry anonymousJobHandlerRegistry,
IOptions<AbpBackgroundJobOptions> backgroundJobOptions,
IJsonSerializer jsonSerializer)
public RabbitMqBackgroundJobManager(IJobQueueManager jobQueueManager)
{
JobQueueManager = jobQueueManager;
AnonymousJobHandlerRegistry = anonymousJobHandlerRegistry;
BackgroundJobOptions = backgroundJobOptions.Value;
JsonSerializer = jsonSerializer;
Logger = NullLogger<RabbitMqBackgroundJobManager>.Instance;
_jobQueueManager = jobQueueManager;
}
public virtual async Task<string> EnqueueAsync<TArgs>(
public async Task<string> EnqueueAsync<TArgs>(
TArgs args,
BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null)
{
var jobQueue = await JobQueueManager.GetAsync<TArgs>();
var jobQueue = await _jobQueueManager.GetAsync<TArgs>();
return (await jobQueue.EnqueueAsync(args, priority, delay))!;
}
public virtual async Task<string> EnqueueAsync(
string jobName,
object args,
BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null)
{
if (ShouldWrapAsAnonymousJob(jobName))
{
Logger.LogInformation(
"Wrapping job into anonymous transport. TransportJobName: {TransportJobName}, EffectiveJobName: {EffectiveJobName}",
AnonymousJobArgs.JobNameConstant,
jobName
);
var jsonData = JsonSerializer.Serialize(args);
var anonymousArgs = new AnonymousJobArgs(jobName, jsonData);
return await EnqueueAsync(AnonymousJobArgs.JobNameConstant, anonymousArgs, priority, delay);
}
var jobQueue = await JobQueueManager.GetAsync(jobName);
return (await jobQueue.EnqueueAsync(args, priority, delay))!;
}
protected virtual bool ShouldWrapAsAnonymousJob(string jobName)
{
return jobName != AnonymousJobArgs.JobNameConstant &&
AnonymousJobHandlerRegistry.IsRegistered(jobName) &&
BackgroundJobOptions.GetJobOrNull(jobName) == null;
}
}

4
framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpBackgroundJobsTickerQModule.cs

@ -54,8 +54,8 @@ public class AbpBackgroundJobsTickerQModule : AbpModule
{
var jobExecuter = serviceProvider.GetRequiredService<IBackgroundJobExecuter>();
var args = await TickerRequestProvider.GetRequestAsync<TArgs>(context, cancellationToken);
var jobConfiguration = options.GetJob(typeof(TArgs));
var jobExecutionContext = new JobExecutionContext(scope.ServiceProvider, jobConfiguration.JobType, args!, cancellationToken: cancellationToken);
var jobType = options.GetJob(typeof(TArgs)).JobType;
var jobExecutionContext = new JobExecutionContext(scope.ServiceProvider, jobType, args!, cancellationToken: cancellationToken);
await jobExecuter.ExecuteAsync(jobExecutionContext);
}
};

68
framework/src/Volo.Abp.BackgroundJobs.TickerQ/Volo/Abp/BackgroundJobs/TickerQ/AbpTickerQBackgroundJobManager.cs

@ -1,85 +1,39 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using TickerQ.Utilities;
using TickerQ.Utilities.Entities;
using TickerQ.Utilities.Interfaces.Managers;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Json;
namespace Volo.Abp.BackgroundJobs.TickerQ;
[Dependency(ReplaceServices = true)]
public class AbpTickerQBackgroundJobManager : IBackgroundJobManager, ITransientDependency
{
private readonly static MethodInfo CreateTickerRequestMethod = typeof(TickerHelper).GetMethod(nameof(TickerHelper.CreateTickerRequest), BindingFlags.Public | BindingFlags.Static)!;
protected ITimeTickerManager<TimeTickerEntity> TimeTickerManager { get; }
protected AbpBackgroundJobOptions Options { get; }
protected AbpBackgroundJobsTickerQOptions TickerQOptions { get; }
protected IAnonymousJobHandlerRegistry AnonymousJobHandlerRegistry { get; }
protected IJsonSerializer JsonSerializer { get; }
public ILogger<AbpTickerQBackgroundJobManager> Logger { get; set; }
public AbpTickerQBackgroundJobManager(
ITimeTickerManager<TimeTickerEntity> timeTickerManager,
IOptions<AbpBackgroundJobOptions> options,
IOptions<AbpBackgroundJobsTickerQOptions> tickerQOptions,
IAnonymousJobHandlerRegistry anonymousJobHandlerRegistry,
IJsonSerializer jsonSerializer)
IOptions<AbpBackgroundJobsTickerQOptions> tickerQOptions)
{
TimeTickerManager = timeTickerManager;
Options = options.Value;
TickerQOptions = tickerQOptions.Value;
AnonymousJobHandlerRegistry = anonymousJobHandlerRegistry;
JsonSerializer = jsonSerializer;
Logger = NullLogger<AbpTickerQBackgroundJobManager>.Instance;
}
public virtual async Task<string> EnqueueAsync<TArgs>(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)
{
var job = Options.GetJob(typeof(TArgs));
return await EnqueueAsync(job, args!, priority, delay);
}
public virtual async Task<string> EnqueueAsync(string jobName, object args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)
{
if (ShouldWrapAsAnonymousJob(jobName))
{
Logger.LogInformation(
"Wrapping job into anonymous transport. TransportJobName: {TransportJobName}, EffectiveJobName: {EffectiveJobName}",
AnonymousJobArgs.JobNameConstant,
jobName
);
var jsonData = JsonSerializer.Serialize(args);
var anonymousArgs = new AnonymousJobArgs(jobName, jsonData);
return await EnqueueAsync(AnonymousJobArgs.JobNameConstant, anonymousArgs, priority, delay);
}
var job = Options.GetJob(jobName);
return await EnqueueAsync(job, args, priority, delay);
}
protected virtual bool ShouldWrapAsAnonymousJob(string jobName)
{
return jobName != AnonymousJobArgs.JobNameConstant &&
AnonymousJobHandlerRegistry.IsRegistered(jobName) &&
Options.GetJobOrNull(jobName) == null;
}
protected virtual async Task<string> EnqueueAsync(BackgroundJobConfiguration job, object args, BackgroundJobPriority priority, TimeSpan? delay)
{
var normalizedArgs = NormalizeArgs(job.ArgsType, args);
var timeTicker = new TimeTickerEntity
{
Id = Guid.NewGuid(),
Function = job.JobName,
ExecutionTime = delay == null ? DateTime.UtcNow : DateTime.UtcNow.Add(delay.Value),
Request = CreateTickerRequest(job.ArgsType, normalizedArgs),
Request = TickerHelper.CreateTickerRequest<TArgs>(args),
};
var config = TickerQOptions.GetConfigurationOrNull(job.JobType);
@ -93,22 +47,4 @@ public class AbpTickerQBackgroundJobManager : IBackgroundJobManager, ITransientD
var result = await TimeTickerManager.AddAsync(timeTicker);
return !result.IsSucceeded ? timeTicker.Id.ToString() : result.Result.Id.ToString();
}
protected virtual object NormalizeArgs(Type argsType, object args)
{
if (argsType.IsInstanceOfType(args))
{
return args;
}
var serialized = JsonSerializer.Serialize(args);
return JsonSerializer.Deserialize(argsType, serialized)!;
}
protected virtual byte[]? CreateTickerRequest(Type argsType, object args)
{
return (byte[]?)CreateTickerRequestMethod
.MakeGenericMethod(argsType)
.Invoke(null, [args]);
}
}

33
framework/src/Volo.Abp.BackgroundJobs/Volo/Abp/BackgroundJobs/DefaultBackgroundJobManager.cs

@ -1,7 +1,5 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DependencyInjection;
@ -20,50 +18,34 @@ public class DefaultBackgroundJobManager : IBackgroundJobManager, ITransientDepe
protected IBackgroundJobSerializer Serializer { get; }
protected IGuidGenerator GuidGenerator { get; }
protected IBackgroundJobStore Store { get; }
protected IAnonymousJobHandlerRegistry AnonymousJobHandlerRegistry { get; }
protected IOptions<AbpBackgroundJobOptions> BackgroundJobOptions { get; }
protected IOptions<AbpBackgroundJobWorkerOptions> BackgroundJobWorkerOptions { get; }
public ILogger<DefaultBackgroundJobManager> Logger { get; set; }
public DefaultBackgroundJobManager(
IClock clock,
IBackgroundJobSerializer serializer,
IBackgroundJobStore store,
IGuidGenerator guidGenerator,
IAnonymousJobHandlerRegistry anonymousJobHandlerRegistry,
IOptions<AbpBackgroundJobOptions> backgroundJobOptions,
IOptions<AbpBackgroundJobWorkerOptions> backgroundJobWorkerOptions)
{
Clock = clock;
Serializer = serializer;
GuidGenerator = guidGenerator;
AnonymousJobHandlerRegistry = anonymousJobHandlerRegistry;
BackgroundJobOptions = backgroundJobOptions;
BackgroundJobWorkerOptions = backgroundJobWorkerOptions;
Store = store;
Logger = NullLogger<DefaultBackgroundJobManager>.Instance;
}
public virtual async Task<string> EnqueueAsync<TArgs>(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)
{
var jobName = BackgroundJobOptions.Value.GetBackgroundJobName(typeof(TArgs));
return await EnqueueAsync(jobName, args!, priority, delay);
var jobId = await EnqueueAsync(jobName, args!, priority, delay);
return jobId.ToString();
}
public virtual async Task<string> EnqueueAsync(string jobName, object args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)
protected virtual async Task<Guid> EnqueueAsync(string jobName, object args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null)
{
if (ShouldWrapAsAnonymousJob(jobName))
{
Logger.LogInformation(
"Wrapping job into anonymous transport. TransportJobName: {TransportJobName}, EffectiveJobName: {EffectiveJobName}",
AnonymousJobArgs.JobNameConstant,
jobName
);
var jsonData = Serializer.Serialize(args);
var anonymousArgs = new AnonymousJobArgs(jobName, jsonData);
return await EnqueueAsync(AnonymousJobArgs.JobNameConstant, anonymousArgs, priority, delay);
}
var jobInfo = new BackgroundJobInfo
{
Id = GuidGenerator.Create(),
@ -82,13 +64,6 @@ public class DefaultBackgroundJobManager : IBackgroundJobManager, ITransientDepe
await Store.InsertAsync(jobInfo);
return jobInfo.Id.ToString();
}
protected virtual bool ShouldWrapAsAnonymousJob(string jobName)
{
return jobName != AnonymousJobArgs.JobNameConstant &&
AnonymousJobHandlerRegistry.IsRegistered(jobName) &&
BackgroundJobOptions.Value.GetJobOrNull(jobName) == null;
return jobInfo.Id;
}
}

16
framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/AbpBackgroundJobsTestModule.cs

@ -14,15 +14,17 @@ public class AbpBackgroundJobsTestModule : AbpModule
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddSingleton<AnonymousJobExecutionTracker>();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var dynamicJobManager = context.ServiceProvider.GetRequiredService<IDynamicBackgroundJobManager>();
var tracker = context.ServiceProvider.GetRequiredService<AnonymousJobExecutionTracker>();
Configure<AbpBackgroundJobOptions>(options =>
dynamicJobManager.RegisterHandler("TestAnonymousJob", (ctx, ct) =>
{
options.AddAnonymousJobHandler("TestAnonymousJob", (context, ct) =>
{
var tracker = context.ServiceProvider.GetRequiredService<AnonymousJobExecutionTracker>();
tracker.ExecutedJsonData.Add(context.JsonData);
return System.Threading.Tasks.Task.CompletedTask;
});
tracker.ExecutedJsonData.Add(ctx.JsonData);
return System.Threading.Tasks.Task.CompletedTask;
});
}
}

51
framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobExecuter_Tests.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Shouldly;
@ -137,54 +136,4 @@ public class BackgroundJobExecuter_Tests : BackgroundJobsTestBase
//Assert
asyncJobObject.Canceled.ShouldBeTrue();
}
[Fact]
public async Task Should_Execute_Anonymous_Job_Handler()
{
var tracker = GetRequiredService<AnonymousJobExecutionTracker>();
tracker.ExecutedJsonData.ShouldBeEmpty();
var args = new AnonymousJobArgs("TestAnonymousJob", "{\"OrderId\":\"ORD-001\"}");
await _backgroundJobExecuter.ExecuteAsync(
new JobExecutionContext(
ServiceProvider,
typeof(AnonymousJobExecutorAsyncBackgroundJob),
args
)
);
tracker.ExecutedJsonData.Count.ShouldBe(1);
tracker.ExecutedJsonData[0].ShouldContain("ORD-001");
}
[Fact]
public async Task Should_Execute_Anonymous_Job_Handler_Registered_At_Runtime()
{
var handlerRegistry = GetRequiredService<IAnonymousJobHandlerRegistry>();
var executedValues = new List<string>();
handlerRegistry.Register("RuntimeAnonymousJob", (context, ct) =>
{
executedValues.Add(context.JsonData);
return Task.CompletedTask;
});
var args = new AnonymousJobArgs("RuntimeAnonymousJob", "{\"Message\":\"hello-runtime\"}");
await _backgroundJobExecuter.ExecuteAsync(
new JobExecutionContext(
ServiceProvider,
typeof(AnonymousJobExecutorAsyncBackgroundJob),
args
)
);
executedValues.Count.ShouldBe(1);
executedValues[0].ShouldContain("hello-runtime");
handlerRegistry.IsRegistered("RuntimeAnonymousJob").ShouldBeTrue();
handlerRegistry.Unregister("RuntimeAnonymousJob").ShouldBeTrue();
handlerRegistry.IsRegistered("RuntimeAnonymousJob").ShouldBeFalse();
}
}

38
framework/test/Volo.Abp.BackgroundJobs.Tests/Volo/Abp/BackgroundJobs/BackgroundJobManager_Tests.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Shouldly;
using Xunit;
@ -9,14 +8,14 @@ namespace Volo.Abp.BackgroundJobs;
public class BackgroundJobManager_Tests : BackgroundJobsTestBase
{
private readonly IBackgroundJobManager _backgroundJobManager;
private readonly IDynamicBackgroundJobManager _dynamicBackgroundJobManager;
private readonly IBackgroundJobStore _backgroundJobStore;
private readonly IAnonymousJobHandlerRegistry _anonymousJobHandlerRegistry;
public BackgroundJobManager_Tests()
{
_backgroundJobManager = GetRequiredService<IBackgroundJobManager>();
_dynamicBackgroundJobManager = GetRequiredService<IDynamicBackgroundJobManager>();
_backgroundJobStore = GetRequiredService<IBackgroundJobStore>();
_anonymousJobHandlerRegistry = GetRequiredService<IAnonymousJobHandlerRegistry>();
}
[Fact]
@ -39,7 +38,7 @@ public class BackgroundJobManager_Tests : BackgroundJobsTestBase
public async Task Should_Store_Jobs_With_JobName()
{
var jobName = BackgroundJobNameAttribute.GetName<MyJobArgs>();
var jobIdAsString = await _backgroundJobManager.EnqueueAsync(jobName, (object)new
var jobIdAsString = await _dynamicBackgroundJobManager.EnqueueAsync(jobName, new
{
Value = "42"
});
@ -54,10 +53,7 @@ public class BackgroundJobManager_Tests : BackgroundJobsTestBase
public async Task Should_Store_Async_Jobs_With_JobName()
{
var jobName = BackgroundJobNameAttribute.GetName<MyAsyncJobArgs>();
var jobIdAsString = await _backgroundJobManager.EnqueueAsync(jobName, (object)new Dictionary<string, object>()
{
["Value"] = "42"
});
var jobIdAsString = await _dynamicBackgroundJobManager.EnqueueAsync(jobName, new { Value = "42" });
jobIdAsString.ShouldNotBe(default);
var jobInfo = await _backgroundJobStore.FindAsync(Guid.Parse(jobIdAsString));
@ -68,7 +64,7 @@ public class BackgroundJobManager_Tests : BackgroundJobsTestBase
[Fact]
public async Task Should_Store_Anonymous_Jobs()
{
var jobIdAsString = await _backgroundJobManager.EnqueueAsync("TestAnonymousJob", new { OrderId = "ORD-001" });
var jobIdAsString = await _dynamicBackgroundJobManager.EnqueueAsync("TestAnonymousJob", new { OrderId = "ORD-001" });
jobIdAsString.ShouldNotBe(default);
var jobInfo = await _backgroundJobStore.FindAsync(Guid.Parse(jobIdAsString));
@ -82,9 +78,9 @@ public class BackgroundJobManager_Tests : BackgroundJobsTestBase
public async Task Should_Not_Wrap_If_Typed_Job_Exists_For_Same_Name()
{
var typedJobName = BackgroundJobNameAttribute.GetName<MyJobArgs>();
_anonymousJobHandlerRegistry.Register(typedJobName, (_, _) => Task.CompletedTask);
_dynamicBackgroundJobManager.RegisterHandler(typedJobName, (_, _) => Task.CompletedTask);
var jobIdAsString = await _backgroundJobManager.EnqueueAsync(typedJobName, new { Value = "42" });
var jobIdAsString = await _dynamicBackgroundJobManager.EnqueueAsync(typedJobName, new { Value = "42" });
jobIdAsString.ShouldNotBe(default);
var jobInfo = await _backgroundJobStore.FindAsync(Guid.Parse(jobIdAsString));
@ -92,4 +88,24 @@ public class BackgroundJobManager_Tests : BackgroundJobsTestBase
jobInfo.JobName.ShouldBe(typedJobName);
jobInfo.JobName.ShouldNotBe(AnonymousJobArgs.JobNameConstant);
}
[Fact]
public async Task Should_Throw_For_Unknown_Job_Name()
{
await Assert.ThrowsAsync<AbpException>(() =>
_dynamicBackgroundJobManager.EnqueueAsync("NonExistentJob", new { Value = "42" })
);
}
[Fact]
public void Should_Register_And_Unregister_Handler()
{
_dynamicBackgroundJobManager.IsHandlerRegistered("TestDynamic").ShouldBeFalse();
_dynamicBackgroundJobManager.RegisterHandler("TestDynamic", (_, _) => Task.CompletedTask);
_dynamicBackgroundJobManager.IsHandlerRegistered("TestDynamic").ShouldBeTrue();
_dynamicBackgroundJobManager.UnregisterHandler("TestDynamic").ShouldBeTrue();
_dynamicBackgroundJobManager.IsHandlerRegistered("TestDynamic").ShouldBeFalse();
}
}

29
modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/DemoAppSharedModule.cs

@ -11,23 +11,26 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Shared
[DependsOn(typeof(AbpMultiTenancyModule))]
public class DemoAppSharedModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
Configure<AbpBackgroundJobOptions>(options =>
var dynamicJobManager = context.ServiceProvider.GetService<IDynamicBackgroundJobManager>();
if (dynamicJobManager == null)
{
options.AddAnonymousJobHandler("CompileTimeAnonymousJob", (ctx, ct) =>
return;
}
dynamicJobManager.RegisterHandler("CompileTimeAnonymousJob", (ctx, ct) =>
{
using (var doc = JsonDocument.Parse(ctx.JsonData))
{
using (var doc = JsonDocument.Parse(ctx.JsonData))
{
var value = doc.RootElement.TryGetProperty("value", out var prop)
var value = doc.RootElement.TryGetProperty("value", out var prop)
? prop.GetString()
: doc.RootElement.TryGetProperty("Value", out prop)
? prop.GetString()
: doc.RootElement.TryGetProperty("Value", out prop)
? prop.GetString()
: null;
Console.WriteLine($"[ANONYMOUS-COMPILE] {value}");
return Task.CompletedTask;
}
});
: null;
Console.WriteLine($"[ANONYMOUS-COMPILE] {value}");
return Task.CompletedTask;
}
});
}

47
modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Jobs/SampleJobCreator.cs

@ -9,14 +9,14 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Shared.Jobs
public class SampleJobCreator : ITransientDependency
{
private readonly IBackgroundJobManager _backgroundJobManager;
private readonly IAnonymousJobHandlerRegistry _anonymousJobHandlerRegistry;
private readonly IDynamicBackgroundJobManager? _dynamicBackgroundJobManager;
public SampleJobCreator(
IBackgroundJobManager backgroundJobManager,
IAnonymousJobHandlerRegistry anonymousJobHandlerRegistry)
IDynamicBackgroundJobManager? dynamicBackgroundJobManager = null)
{
_backgroundJobManager = backgroundJobManager;
_anonymousJobHandlerRegistry = anonymousJobHandlerRegistry;
_dynamicBackgroundJobManager = dynamicBackgroundJobManager;
}
public void CreateJobs()
@ -26,7 +26,17 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Shared.Jobs
public async Task CreateJobsAsync()
{
_anonymousJobHandlerRegistry.Register("RuntimeAnonymousJob", (context, ct) =>
// Type-safe enqueue (existing)
await _backgroundJobManager.EnqueueAsync(new WriteToConsoleGreenJobArgs { Value = "test 1 (green) - typed" });
await _backgroundJobManager.EnqueueAsync(new WriteToConsoleYellowJobArgs { Value = "test 1 (yellow) - typed" });
if (_dynamicBackgroundJobManager == null)
{
return;
}
// Register runtime dynamic handler
_dynamicBackgroundJobManager.RegisterHandler("RuntimeAnonymousJob", (context, ct) =>
{
using (var doc = JsonDocument.Parse(context.JsonData))
{
@ -40,35 +50,32 @@ namespace Volo.Abp.BackgroundJobs.DemoApp.Shared.Jobs
}
});
// Type-safe enqueue (existing)
await _backgroundJobManager.EnqueueAsync(new WriteToConsoleGreenJobArgs { Value = "test 1 (green) - typed" });
await _backgroundJobManager.EnqueueAsync(new WriteToConsoleYellowJobArgs { Value = "test 1 (yellow) - typed" });
// String-based enqueue with strongly-typed args
await _backgroundJobManager.EnqueueAsync(
// String-based enqueue with typed job (by name)
await _dynamicBackgroundJobManager.EnqueueAsync(
"GreenJob",
(object)new WriteToConsoleGreenJobArgs { Value = "test 2 (green) - by name, typed args" }
new WriteToConsoleGreenJobArgs { Value = "test 2 (green) - by name, typed args" }
);
await _backgroundJobManager.EnqueueAsync(
await _dynamicBackgroundJobManager.EnqueueAsync(
"YellowJob",
(object)new WriteToConsoleYellowJobArgs { Value = "test 2 (yellow) - by name, typed args" }
new WriteToConsoleYellowJobArgs { Value = "test 2 (yellow) - by name, typed args" }
);
// String-based enqueue with anonymous object
await _backgroundJobManager.EnqueueAsync(
// String-based enqueue with anonymous object (typed job path)
await _dynamicBackgroundJobManager.EnqueueAsync(
"GreenJob",
(object)new { Value = "test 3 (green) - by name, anonymous", Time = DateTime.Now }
new { Value = "test 3 (green) - by name, anonymous", Time = DateTime.Now }
);
await _backgroundJobManager.EnqueueAsync(
await _dynamicBackgroundJobManager.EnqueueAsync(
"YellowJob",
(object)new { Value = "test 3 (yellow) - by name, anonymous", Time = DateTime.Now }
new { Value = "test 3 (yellow) - by name, anonymous", Time = DateTime.Now }
);
await _backgroundJobManager.EnqueueAsync(
// Dynamic job handlers
await _dynamicBackgroundJobManager.EnqueueAsync(
"CompileTimeAnonymousJob",
new { Value = "test 4 (anonymous) - compile-time" }
);
await _backgroundJobManager.EnqueueAsync(
await _dynamicBackgroundJobManager.EnqueueAsync(
"RuntimeAnonymousJob",
new { Value = "test 5 (anonymous) - runtime" }
);

Loading…
Cancel
Save