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

时间:2013-09-17 19:41:25

标签: c# scheduled-tasks console-application

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

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

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

我认为服务,但这似乎有点过分。

8 个答案:

答案 0 :(得分:26)

我的建议是编写使用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 */ }
}

答案 1 :(得分:4)

您尝试实施轮询方法,其中作业正在监视数据库中的记录以进行任何更改。

在这种情况下,我们试图定期敲击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

答案 2 :(得分:4)

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

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

对于fire-and-forget类型的任务,消息队列非常适合。如果目的地不可靠,则必须选择重试级别(例如,尝试发送(最多两次),5分钟后重试,尝试发送(最多两次),15分钟后重试)至少需要指定消息特定的TTL与发送和重试队列。 Here's an explanation with a link to code to setup a retry level queue

托管预调度任务将要求您使用数据库队列方法(Click here for a CodeProject article on designing a database queue for scheduled tasks) 。这将允许您更新,删除或重新安排通知,因为您可以跟踪所有权标识符(例如,指定用户ID,并且当用户不再接收诸如已故/取消订阅的通知时,您可以删除所有待处理的通知)

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

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

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

修改

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

答案 3 :(得分:2)

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

答案 4 :(得分:1)

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

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

答案 5 :(得分:1)

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

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

答案 6 :(得分:1)

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

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

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

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

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

答案 7 :(得分:1)

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

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

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

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

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

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

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

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