C#console app在预定时间发送电子邮件

我在Windows Server 2003上运行了一个C#控制台应用程序,其目的是读取一个名为Notifications的表和一个名为“NotifyDateTime”的字段,并在达到该时间时发送一封电子邮件。 我通过任务计划程序安排它按小时运行,检查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 ScheduleMap = new Dictionary { {"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); } } 

输出:

在此处输入图像描述

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

您尝试实现轮询方法,其中作业正在监视DB中的记录以查找任何更改。

在这种情况下,我们试图在周期性时间内击中DB,因此如果一小时延迟减少到1分钟后期,则此解决方案将变为性能瓶颈。

方法1

对于这种情况,请使用基于队列的方法来避免任何问题,如果您要发送这么多电子邮件,也可以扩展实例数。

据我所知,有一个程序在表中更新NotifyDateTime ,同一个程序可以将消息推送到队列,通知有一个通知要处理。

当有消息执行所需的操作(即发送电子邮件)时,有一个Windows服务正在查看此队列中的任何传入消息。

方法2

http://msdn.microsoft.com/en-us/library/vstudio/zxsa8hkf(v=vs.100).aspx

如果您使用的是MS SQL Server,还可以从SQL Server存储过程调用C#代码。 但在这种情况下,您正在利用SQL服务器进程发送邮件,这不是一个好习惯。

但是,您可以调用可以发送电子邮件的Web服务或WCF服务。

但是方法1没有错误,可扩展,可跟踪,异步,并且不会给您的数据库或APP带来麻烦,您有不同的发送电子邮件的过程。

队列

使用MSMQ,它是Windows服务器的一部分

您也可以尝试https://www.rabbitmq.com/dotnet.html

预定任务(在未定义的时间)通常很难处理,而Quartz.NET似乎很适合的预定任务。

此外,对于不应中断/更改的任务(例如重试,通知)和需要主动管理的任务(例如,活动或通信),要做出另外一个区别。

对于fire-and-forget类型的任务,消息队列非常适合。 如果目的地不可靠,则必须选择重试级别(例如,尝试发送(最多两次),5分钟后重试,尝试发送(最多两次),15分钟后重试)至少需要指定消息特定的TTL与发送和重试队列。 这是一个解释,其中包含设置重试级别队列的代码链接

托管预先安排的任务将要求您使用数据库队列方法( 单击此处查看有关为计划任务设计数据库队列的CodeProject文章 )。 这将允许您更新,删除或重新安排通知,因为您可以跟踪所有权标识符(例如,指定用户ID,并且当用户不再接收诸如已故/取消订阅的通知时,您可以删除所有待处理的通知)

计划的电子邮件任务(包括任何通信任务)需要更精细的控制(到期,重试和超时机制)。 这里采取的最佳方法是构建一个能够通过其步骤处理电子邮件任务的状态机(过期,预validation,预先邮寄步骤,如模板化,内联css,使链接绝对,添加跟踪对象)用于开放式跟踪,缩短用于点击跟踪,后validation以及发送和重试的链接。

希望您知道.NET SmtpClient不完全符合MIME规范,并且您应该使用SAAS电子邮件提供程序,如Amazon SES,Mandrill,Mailgun,Customer.io或Sendgrid。 我建议你看看Mandrill或Mailgun。 此外,如果您有时间,请查看MimeKit ,您可以使用它为供应商构建MIME消息,允许发送原始电子邮件,但不一定支持附件/自定义标头/ DKIM签名等内容。

我希望这能让你走上正确的道路。

编辑

您必须使用服务以特定间隔(例如15秒或1分钟)进行轮询。 通过一次签出一定数量的到期任务并保留发送的内部消息池(使用超时机制),可以在一定程度上抵消数据库负载。 当没有返回消息时,只需“暂停”轮询一段时间。 我建议不要针对数据库中的单个表构建这样的系统 – 而是设计一个可以与之集成的独立电子邮件调度系统。

我会把它变成服务而不是。 您可以为每个计划时间使用System.Threading.Timer事件处理程序。

计划任务可以安排在特定时间运行一次(而不是每小时,每天等),因此一个选项是在数据库中的特定字段更改时创建计划任务。

您没有提到您使用的数据库,但有些数据库支持触发器的概念,例如在SQL中: http : //technet.microsoft.com/en-us/library/ms189799.aspx

如果您知道何时需要提前发送电子邮件,那么我建议您使用适当的超时对事件句柄进行等待。 在午夜看表,然后等待事件句柄,超时设置为在需要发送下一封电子邮件时到期。 在发送电子邮件之后再次等待,并根据应发送的下一封邮件设置超时。

此外,根据您的描述,这可能应该作为服务实现,但不是必需的。

三年前我一直在处理同样的问题。 我已经好几次改变了这个过程,我告诉你原因:

  1. 第一个实现是使用来自虚拟主机的特殊守护程序,称为IIS网站。 网站检查了来电者IP,然后检查数据库并发送电子邮件。 这一直工作到有一天,当我从用户收到很多非常脏的电子邮件时 ,我已经完全垃圾邮件他们的邮箱了。 将电子邮件保留在数据库中并从SMTP电子邮件发送的缺点是没有确保数据库到SMTP事务的NOTHING 。 您永远不确定电子邮件是否已成功发送。 发送电子邮件可能是成功的,可能会失败,也可能是误报或可能是假阴性(SMTP客户端告诉您,电子邮件未发送,但确实如此)。 SMTP服务器出现问题,服务器返回false(电子邮件未发送),但电子邮件已发送。 守护程序在脏电子邮件出现前一整天每小时重新发送一次电子邮件。

  2. 第二个实现:为防止发送垃圾邮件,我更改了算法,即使失败也认为电子邮件被发送(我的电子邮件通知不是太重要)。 我的第一个建议是:“不要经常启动deamon,因为这种错误的负smtp错误会让用户感到不安。”

  3. 几个月后,服务器上发生了一些变化,守护进程运行不正常。 我从stackoverflow中得到了这个想法:将.NET计时器绑定到Web应用程序域。 这不是一个好主意,因为似乎IIS可以不时地重新启动应用程序,因为内存泄漏,如果重新启动更频繁,那么计时器永远不会触发计时器滴答。

  4. 最后一次实施。 Windows调度程序每小时触发读取本地网站的python批处理。 这火了ASP.NET代码。 优点是时间窗调度程序可靠地调用本地批处理和网站。 IIS没有挂起,它具有重启能力。 计时器网站是我网站的一部分,它仍然是一个项目。 (您可以使用控制台应用程序)。 简单就是更好。 它只是工作!

在我看来,你的第一选择是正确的选择。 任务计划程序是MS推荐的执行定期作业的方法。 此外,它灵活,可以报告操作失败,优化并在系统中的所有任务中摊销,…

创建任何一直运行的控制台类应用程序是脆弱的。 它可以被任何人关闭,需要一个开放的视野,不会自动重启,……

另一种选择是创建某种服务。 它保证一直在运行,所以至少可以工作。 但你的动机是什么?

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

哦是的优化…所以你想为你的计算机添加一个新的永久运行服务,以便你每小时避免一个可能不需要的SQL查询? 治愈看起来比疾病更糟糕。

我没有提到该服务的所有缺点。 一方面,您的任务在不运行时不使用任何资源。 它非常简单,轻量级且查询效率高(只要您拥有正确的索引)。

另一方面,如果你的服务崩溃,它可能会好转。 它需要一种方式来通知可能需要比当前安排的更早发送的新电子邮件。 它永久使用计算机资源,例如内存。 更糟糕的是,它可能包含内存泄漏。

我认为除了微不足道的周期性任务之外,任何解决方案的成本/效益比都非常低。