C# 控制台应用程序在预定时间发送电子邮件

标签 c# scheduled-tasks console-application

我有一个在 Windows Server 2003 上运行的 C# 控制台应用程序,其目的是读取一个名为 Notifications 的表和一个名为“NotifyDateTime”的字段,并在到达该时间时发送一封电子邮件。我通过 Task Scheduler 安排它每小时运行一次,检查 NotifyDateTime 是否在那个小时内,然后发送通知。

似乎因为我在数据库中有通知日期/时间,所以应该有比每小时重新运行这个东西更好的方法。

是否有一个我可以在服务器上运行的轻量级进程/控制台应用程序,它从表中读取当天的通知并在它们到期时准确发出它们?

我认为是服务,但这似乎有些过分了。

最佳答案

我的建议是编写简单的应用程序,使用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);
    }
}

输出:

enter image description here

如果您要使用服务,只需将此主要逻辑放入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 */ }
}

关于C# 控制台应用程序在预定时间发送电子邮件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18858512/

相关文章:

c# - 用于解析完全限定类型名称的现有类/方法

c# - WPF UserControl 检测 LostFocus 忽略 child

java - 如何在 Spring 使用 EnableScheduling 注释在运行时重新启动计划任务?

Android - 使用 Timer 和 TimerTask 控制任务?

c# - 在 C# 中执行控制台应用程序?

c# - VS 控制台应用程序 - 将所有库编译成 exe

c# - 在 ASP.NET Core 中也延迟加载后虚拟属性未加载

c# - 从串行端口读取影响值

linux - 从里面找出 'at'作业的ID

c# - 是否可以从控制台应用程序发送 Toast 通知?