C# console app to send email at scheduled times

前端 未结 8 708
北海茫月
北海茫月 2020-12-28 18:44

I\'ve got a C# console app running on Windows Server 2003 whose purpose is to read a table called Notifications and a field called \"NotifyDateTime\" and send an email when

相关标签:
8条回答
  • 2020-12-28 19:02

    I would turn it into a service instead. You can use System.Threading.Timer event handler for each of the scheduled times.

    0 讨论(0)
  • 2020-12-28 19:09

    Your first choice is the correct option in my opinion. Task Scheduler is the MS recommended way to perform periodic jobs. Moreover it's flexible, can reports failures to ops, is optimized and amortized amongst all tasks in the system, ...

    Creating any console-kind app that runs all the time is fragile. It can be shutdown by anyone, needs an open seesion, doesn't restart automatically, ...

    The other option is creating some kind of service. It's guaranteed to be running all the time, so that would at least work. But what was your motivation?

    "It seems like because I have the notification date/times in the database that there should be a better way than re-running this thing every hour."

    Oh yeah optimization... So you want to add a new permanently running service to your computer so that you avoid one potentially unrequired SQL query every hour? The cure looks worse than the disease to me.

    And I didn't mention all the drawbacks of the service. On one hand, your task uses no resource when it doesn't run. It's very simple, lightweight and the query efficient (provided you have the right index).

    On the other hand, if your service crashes it's probably gone for good. It needs a way to be notified of new e-mails that may need to be sent earlier than what's currently scheduled. It permanently uses computer resources, such as memory. Worse, it may contain memory leaks.

    I think that the cost/benefit ratio is very low for any solution other than the trivial periodic task.

    0 讨论(0)
  • 2020-12-28 19:10

    You trying to implement polling approach, where a job is monitoring a record in DB for any changes.

    In this case we are trying to hit DB for periodic time, so if the one hour delay reduced to 1 min later stage, then this solution turns to performance bottle neck.

    Approach 1

    For this scenario please use Queue based approach to avoid any issues, you can also scale up number of instances if you are sending so many emails.

    I understand there is a program updates NotifyDateTime in a table, the same program can push a message to Queue informing that there is a notification to handle.

    There is a windows service looking after this queue for any incoming messages, when there is a message it performs the required operation (ie sending email).

    Approach 2

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

    you can also invoke C# code from SQL Server Stored procedure if you are using MS SQL Server. but in this case you are making use of your SQL server process to send mail, which is not a good practice.

    However you can invoke a web service, OR WCF service which can send emails.

    But Approach 1 is error free, Scalable , Track-able, Asynchronous , and doesn't trouble your data base OR APP, you have different process to send email.

    Queues

    Use MSMQ which is part of windows server

    You can also try https://www.rabbitmq.com/dotnet.html

    0 讨论(0)
  • 2020-12-28 19:14

    My suggestion is to write simple application, which uses Quartz.NET.

    Create 2 jobs:

    • First, fires once a day, reads all awaiting notification times from database planned for that day, creates some triggers based on them.
    • Second, registered for such triggers (prepared by the first job), sends your notifications.

    What's more,

    I strongly advice you to create windows service for such purpose, just not to have lonely console application constantly running. It can be accidentally terminated by someone who have access to the server under the same account. What's more, if the server will be restarted, you have to remember to turn such application on again, manually, while the service can be configured to start automatically.

    If you're using web application you can always have this logic hosted e.g. within IIS Application Pool process, although it is bad idea whatsoever. It's because such process is by default periodically restarted, so you should change its default configuration to be sure it is still working in the middle of the night, when application is not used. Unless your scheduled tasks will be terminated.

    UPDATE (code samples):

    Manager class, internal logic for scheduling and unscheduling jobs. For safety reasons implemented as a singleton:

    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);
        }
    }
    

    First job, for gathering required information from the database and scheduling notifications (second job):

    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 */ }           
        }
    }
    

    Second job, for sending emails:

    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);
        }
    }
    

    Database mock, just for purposes of this particular example:

    internal class DbMock
    {
        public static IDictionary<string, string> ScheduleMap = 
            new Dictionary<string, string>
            {
                {"00:01", "foo@gmail.com"},
                {"00:02", "bar@yahoo.com"}
            };
    }
    

    Main entry of the application:

    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);
        }
    }
    

    Output:

    enter image description here

    If you're going to use service, just put this main logic to the OnStart method (I advice to start the actual logic in a separate thread not to wait for the service to start, and the same avoid possible timeouts - not in this particular example obviously, but in general):

    protected override void OnStart(string[] args)
    {
        try
        {
            var thread = new Thread(x => WatchThread(new ThreadStart(FireStarter.Execute)));
            thread.Start();
        }
        catch (Exception e) { /* log error */ }            
    }
    

    If so, encapsulate the logic in some wrapper e.g. WatchThread which will catch any errors from the thread:

    private void WatchThread(object pointer)
    {
        try
        {
            ((Delegate) pointer).DynamicInvoke();
        }
        catch (Exception e) { /* log error and stop service */ }
    }
    
    0 讨论(0)
  • 2020-12-28 19:16

    If you know when the emails need to be sent ahead of time then I suggest that you use a wait on an event handle with the appropriate timeout. At midnight look at the table then wait on an event handle with the timeout set to expire when the next email needs to be sent. After sending the email wait again with the timeout set based on the next mail that should be sent.

    Also, based on your description, this should probably be implemented as a service but it is not required.

    0 讨论(0)
  • 2020-12-28 19:18

    Pre-scheduled tasks (at undefined times) are generally a pain to handle, as opposed to scheduled tasks where Quartz.NET seems well suited.

    Furthermore, another distinction is to be made between fire-and-forget for tasks that shouldn't be interrupted/change (ex. retries, notifications) and tasks that need to be actively managed (ex. campaign or communications).

    For the fire-and-forget type tasks a message queue is well suited. If the destination is unreliable, you will have to opt for retry levels (ex. try send (max twice), retry after 5 minutes, try send (max twice), retry after 15 minutes) that at least require specifying message specific TTL's with a send and retry queue. Here's an explanation with a link to code to setup a retry level queue

    The managed pre-scheduled tasks will require that you use a database queue approach (Click here for a CodeProject article on designing a database queue for scheduled tasks) . This will allow you to update, remove or reschedule notifications given you keep track of ownership identifiers (ex. specifiy a user id and you can delete all pending notifications when the user should no longer receive notifications such as being deceased/unsubscribed)

    Scheduled e-mail tasks (including any communication tasks) require finer grained control (expiration, retry and time-out mechanisms). The best approach to take here is to build a state machine that is able to process the e-mail task through its steps (expiration, pre-validation, pre-mailing steps such as templating, inlining css, making links absolute, adding tracking objects for open tracking, shortening links for click tracking, post-validation and sending and retrying).

    Hopefully you are aware that the .NET SmtpClient isn't fully compliant with the MIME specifications and that you should be using a SAAS e-mail provider such as Amazon SES, Mandrill, Mailgun, Customer.io or Sendgrid. I'd suggest you look at Mandrill or Mailgun. Also if you have some time, take a look at MimeKit which you can use to construct MIME messages for the providers allow sending raw e-mail and doesn't necessarily support things like attachments/custom headers/DKIM signing.

    I hope this sets you on the right path.

    Edit

    You will have to use a service to poll at specific intervals (ex. 15 seconds or 1 minute). The database load can be somewhat negated by checkout out a certain amount of due tasks at a time and keeping an internal pool of messages due for sending (with a time-out mechanism in place). When there's no messages returned, just 'sleep' the polling for a while. I'd would advise against building such a system out against a single table in a database - instead design an independant e-mail scheduling system that you can integrate with.

    0 讨论(0)
提交回复
热议问题