我的建议是编写使用Quartz.NET的简单应用程序。
创建 2 个工作:
- 首先,每天触发一次,从当天计划的数据库中读取所有等待通知时间,并根据它们创建一些触发器。
- 其次,注册此类触发器(由第一份工作准备),发送您的通知。
更重要的是,
我强烈建议您为此目的创建 Windows 服务,只是不要让孤独的控制台应用程序不断运行。它可能会被同一帐户下有权访问服务器的人意外终止。更重要的是,如果服务器将重新启动,您必须记住手动重新打开此类应用程序,同时可以将服务配置为自动启动。
如果您使用的是 Web 应用程序,您总是可以将这个逻辑托管在例如 IIS 应用程序池进程中,尽管这不是一个好主意。这是因为默认情况下此类进程会定期重新启动,因此您应该更改其默认配置以确保它在半夜不使用应用程序时仍在工作。除非您的计划任务将被终止。
更新(代码示例):
Manager 类,用于调度和取消调度作业的内部逻辑。出于安全原因,作为单例实现:
internal class ScheduleManager
{
private static readonly ScheduleManager _instance = new ScheduleManager();
private readonly IScheduler _scheduler;
private ScheduleManager()
{
var properties = new NameValueCollection();
properties["quartz.scheduler.instanceName"] = "notifier";
properties["quartz.threadPool.type"] = "Quartz.Simpl.SimpleThreadPool, Quartz";
properties["quartz.threadPool.threadCount"] = "5";
properties["quartz.threadPool.threadPriority"] = "Normal";
var sf = new StdSchedulerFactory(properties);
_scheduler = sf.GetScheduler();
_scheduler.Start();
}
public static ScheduleManager Instance
{
get { return _instance; }
}
public void Schedule(IJobDetail job, ITrigger trigger)
{
_scheduler.ScheduleJob(job, trigger);
}
public void Unschedule(TriggerKey key)
{
_scheduler.UnscheduleJob(key);
}
}
第一项工作,用于从数据库收集所需信息并安排通知(第二项工作):
internal class Setup : IJob
{
public void Execute(IJobExecutionContext context)
{
try
{
foreach (var kvp in DbMock.ScheduleMap)
{
var email = kvp.Value;
var notify = new JobDetailImpl(email, "emailgroup", typeof(Notify))
{
JobDataMap = new JobDataMap {{"email", email}}
};
var time = new DateTimeOffset(DateTime.Parse(kvp.Key).ToUniversalTime());
var trigger = new SimpleTriggerImpl(email, "emailtriggergroup", time);
ScheduleManager.Instance.Schedule(notify, trigger);
}
Console.WriteLine("{0}: all jobs scheduled for today", DateTime.Now);
}
catch (Exception e) { /* log error */ }
}
}
第二份工作,用于发送电子邮件:
internal class Notify: IJob
{
public void Execute(IJobExecutionContext context)
{
try
{
var email = context.MergedJobDataMap.GetString("email");
SendEmail(email);
ScheduleManager.Instance.Unschedule(new TriggerKey(email));
}
catch (Exception e) { /* log error */ }
}
private void SendEmail(string email)
{
Console.WriteLine("{0}: sending email to {1}...", DateTime.Now, email);
}
}
数据库模拟,仅出于此特定示例的目的:
internal class DbMock
{
public static IDictionary<string, string> ScheduleMap =
new Dictionary<string, string>
{
{"00:01", "foo@gmail.com"},
{"00:02", "bar@yahoo.com"}
};
}
应用程序的主要入口:
public class Program
{
public static void Main()
{
FireStarter.Execute();
}
}
public class FireStarter
{
public static void Execute()
{
var setup = new JobDetailImpl("setup", "setupgroup", typeof(Setup));
var midnight = new CronTriggerImpl("setuptrigger", "setuptriggergroup",
"setup", "setupgroup",
DateTime.UtcNow, null, "0 0 0 * * ?");
ScheduleManager.Instance.Schedule(setup, midnight);
}
}
输出:
如果您要使用service,只需将此主要逻辑放入OnStart
方法中(我建议在单独的线程中启动实际逻辑,不要等待服务启动,同样避免可能的超时 - 不在此特定示例中显然,但总的来说):
protected override void OnStart(string[] args)
{
try
{
var thread = new Thread(x => WatchThread(new ThreadStart(FireStarter.Execute)));
thread.Start();
}
catch (Exception e) { /* log error */ }
}
如果是这样,将逻辑封装在一些包装器中,例如 WatchThread,它将捕获线程中的任何错误:
private void WatchThread(object pointer)
{
try
{
((Delegate) pointer).DynamicInvoke();
}
catch (Exception e) { /* log error and stop service */ }
}