我一直在 Microsoft MVC 应用程序中使用 Hangfire。我已经用它来编译和安排“即发即弃”任务,但令我惊讶的是,我无法在程序运行时添加/删除作业。 Hangfire是否真的无法在运行时动态调度任务?是否有一个众所周知的框架,即使在应用程序已编译或部署之后,也允许人们安排任务,而无需每次要添加任务时都更改 C# 代码?
我也研究过Quartz.NET,似乎也有同样的问题。
编辑:
Windows 任务计划程序可以允许使用 GUI 来计划任务,UNIX 的 cron 可以通过编辑文件来添加或删除任务,但我正在寻找某种在 Windows 上运行的应用程序,允许用户添加或删除任务应用程序部署之后的任务。我不想每次要添加或删除任务时都重新编译应用程序。
正如所问,这个问题似乎源于对“动态......在运行时”含义的误解。答案是“是的”,它可以更改任务而无需重新部署(但这似乎不是您真正想要的)。
Hangfire 会向您的应用程序添加仪表板 UI(如果您对其进行配置),但它本身并不是端到端任务管理应用程序。它的设计目的是让“您的应用程序”能够安排工作,并以与调用点非常脱节的方式完成该工作——甚至可能无法在同一台机器上完成。 它仅限于调用 .NET 代码,但
根据定义这满足了您所声明的“在运行时动态调度任务”的要求。这可以响应应用程序中您喜欢的任何事件来完成。任务也可以被删除、更新和取消。 (
后期编辑)你是对的:任何调度 UI 或任务文件格式的反序列化都必须你自己编写。如果您正在寻找一个为您提供 UI 和/或任务文件 OOTB 的工具,您可能需要升级到像 JAMS 这样的商业产品。 (免责声明:这甚至可能本身不具备您需要的功能——我没有对该产品的直接经验,但与我共事过的人都积极地提到过它)。
例如,假设您的代码中有硬编码的任务 A 和任务 B,并且您希望安排它们使用不同的参数动态运行。您可以创建一个 API,该 API 将使用您选择的参数在指定时间运行所需的任务。
[HttpPost]
public IHttpActionResult Post([FromBody]TaskDto task)
{
var job = "";
if(task.TaskName == "TaskA"){
job = BackgroundJob.Schedule(() => RunTaskA(task.p1,task.p2), task.StartTime);
}
if(task.TaskName == "TaskB"){
job = BackgroundJob.Schedule(() => RunTaskB(task.p1,task.p2), task.StartTime);
}
if(!string.IsNullOrWhiteSpace(task.ContinueWith) && !string.IsNullOrWhiteSpace(job)){
if(task.ContinueWith == "TaskB"){
BackgroundJob.ContinueWith(job, () => RunTaskB(task.p3,task.p4));
}
if(task.ContinueWith == "TaskA"){
BackgroundJob.ContinueWith(job, () => RunTaskA(task.p3,task.p4));
}
}
return Ok(job)
}
然后您可以使用 JSON POST 调用 API(使用 javascript 的示例)
// Sending JSON data to start scheduled task via POST
//
var xhr = new XMLHttpRequest();
var url = "https://www.example.com/api/scheduletask";
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var json = JSON.parse(xhr.responseText);
}
};
var data = JSON.stringify({"TaskName": "TaskA", "ContinueWith": "TaskB",
"StartTime": "2-26-2018 10:00 PM", "p1": "myParam1", "p2": true,
"p3": "myParam3", "p4": false});
xhr.send(data);
为了示例的完整性,这里是本示例的 TaskDto 类
public class TaskDto
{
public string TaskName { get; set; }
public string ContinueWith { get; set; }
public DateTime StartTime { get; set; }
public string p1 { get; set; }
public bool p2 { get; set; }
public string p3 { get; set; }
public bool p4 { get; set; }
}
using System;
namespace CustomGist.Hangfire {
public class ActualTimerJob : TimerJob<ActualTimerJob> {
public override JobId => "Actual timer job";
protected override void PerformJobTasks(){
//do the actual job here with full access to _cancellationToken and _context
}
}
}
using Hangfire;
using Hangfire.Server;
using System;
namespace CustomGist.Hangfire {
public interface ITimerJob {
string JobId { get; }
void Execute(IJobCancellationToken cancellationToken, PerformContext context);
void Schedule(string cronExpression);
}
}
using Hangfire;
using Hangfire.Storage;
using System;
namespace CustomGist.Hangfire {
public class Scheduler {
public static void ScheduleRecurringTasks(){
//Type the class name and CRON expression in, but could read this from json or xml if we wanted to
//The class name is fully qualified and could be in another assembly.
string className = "CustomGist.Hangfire.ActualTimerJob";
string cronExpression = Cron.Daily;
try {
var timerJobType = Type.GetType(className);
var timerJobInstance = (ITimerJob) Activator.CreateInstance(timerJobType ?? throw new InvalidOperationException($"Unable to get type {className}"));
timerJobInstance.Schedule(cronExpression);
} catch (Exception e) {
//Handle exception here, if something fails when dynamically loading the type.
}
}
}
}
using Hangfire;
using Hangfire.Server;
using System;
namespace CustomGist.Hangfire {
public abstract class TimerJob<T> : ITimerJob where T : ITimerJob {
public abstract string JobId { get; }
protected IJobCancellationToken _cancellationToken;
protected PerformContext _context;
protected abstract void PerformJobTasks();
[DisableConcurrentExecution(0)]
public void Execute(IJobCancellationToken cancellationToken, PerformContext context) {
_cancellationToken = cancellationToken;
_context = context;
PerformJobTasks();
}
public void Schedule(string cronExpression) {
if (string.IsNullOrWhiteSpace(cronExpression)) {
RecurringJob.RemoveIfExists(JobId);
}
RecurringJob.AddOrUpdate<T>(JobId, x => x.Execute(JobCancellationToken.Null, null), cronExpression, TimeZoneInfo.Local);
}
}
}