views:

52

answers:

2

I was asked to develop an auto-mail sending program on asp.net. It is supposed to send, say 5000 e-mails reading the addresses from a database. It will sure fall into request timeout tough. So it seems I have to convert it into a windows app. But I'd like to know if ajaxifying this web-app would help. If I write a web service, and my web app sends the mail addresses as lists of 50 at a time. when done, send the next 50 and so on. Would this help to solve the problem of http request timeout?

+1  A: 

Using a webservice endpoint to send your emails is a good idea, whether you call it from an aspx class or from the client with javascript.

Simply use the webservice call to spawn a thread to send the emails and return immediately.

If you wanted visual progress cues then write another ajax endpoint or aspx page that will display the status of the email thread's progress.

There are many ways to accomplish this, you should be able to come up with one with the information given.

Batching from ajax is probably going to be more work than you want to do and adds unnecessary complexity (which is never a good thing).

this is interesting. I may spike this and post some code.

Ok, im back. here ya go. both a webform ui and an ajax ui.

None of this is meant to be finished product - is a spike to support a story. bend/fold/spindle at will.

EmailService.asmx

using System;
using System.ComponentModel;
using System.Threading;
using System.Web.Script.Services;
using System.Web.Services;

namespace EmailSendingWebApplication
{
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ToolboxItem(false)]
    [ScriptService]
    public class EmailService : WebService
    {
        private static EmailSendingProgress _currentProgress;
        private static Thread _emailThread;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="criteria">just an example</param>
        /// <param name="anotherCriteria">just an example</param>
        /// <returns></returns>
        [WebMethod]
        public EmailSendingProgress SendEmails(string criteria, int anotherCriteria)
        {
            try
            {
                if (_currentProgress != null && _emailThread.IsAlive)
                {
                    throw new InvalidOperationException(
                        "Email batch is already in progress. Wait for completion or cancel");
                }

                // use your criteria to cue up the emails to be sent.
                // .....
                // and derive a way for a thread to identify the emails
                // i am using a batchid

                int batchId = 1000; // contrived


                // create a thread

                _emailThread = new Thread(ProcessEmails);


                _currentProgress = new EmailSendingProgress
                                       {
                                           Status = ProcessState.Starting,
                                           BatchId = batchId
                                       };


                // you could use a 'state' object but this process/thread
                // is single use/single instance just access _currentProgress
                // by the static member

                _emailThread.Start();


                return _currentProgress;
            }
            catch (Exception ex)
            {
                _currentProgress = new EmailSendingProgress
                           {
                               Status = ProcessState.Error,
                               Message = "Error starting process:" + ex.Message
                           };
            }

            return _currentProgress;
        }

        [WebMethod]
        public EmailSendingProgress CancelEmailProcess()
        {
            if (_currentProgress != null && _emailThread.IsAlive)
            {
                _currentProgress.Cancel = true;
                _currentProgress.Message = "Cancelling";
            }

            return _currentProgress;
        }

        [WebMethod]
        public EmailSendingProgress GetProgress()
        {
            return _currentProgress;
        }

        private static void ProcessEmails()
        {
            // process your emails using the criteria, in this case, 
            // a batchId

            int totalEmails = 100;
            int currentEmail = 0;

            lock (_currentProgress)
            {
                _currentProgress.Total = totalEmails;
                _currentProgress.Status = ProcessState.Processing;
            }


            for (; currentEmail < totalEmails; currentEmail++)
            {
                lock (_currentProgress)
                {
                    if (_currentProgress.Cancel)
                    {
                        _currentProgress.Status = ProcessState.Cancelled;
                        _currentProgress.Message = "User cancelled process.";
                        break;
                    }
                    _currentProgress.Current = currentEmail + 1;
                }

                try
                {
                    // send your email
                    Thread.Sleep(100); // lallalala sending email
                }
                catch (Exception ex)
                {
                    // log the failure in your db

                    // then check to see if we should exit on error
                    // or just keep going.
                    lock (_currentProgress)
                    {
                        if (_currentProgress.CancelBatchOnSendError)
                        {
                            _currentProgress.Status = ProcessState.Error;
                            _currentProgress.Message = ex.Message;
                            break;
                        }
                    }
                }
            }

            {
                // don't want to obscure state/message from abnormal
                // termination..
                if (_currentProgress.Status == ProcessState.Processing)
                {
                    _currentProgress.Status = ProcessState.Idle;
                    _currentProgress.Message = "Processing complete.";
                }
            }
        }
    }

    public enum ProcessState
    {
        Idle,
        Starting,
        Processing,
        Cancelled,
        Error
    }

    [Serializable]
    public class EmailSendingProgress
    {
        public int BatchId;
        public bool Cancel;
        public bool CancelBatchOnSendError;
        public int Current;
        public string Message;
        public ProcessState Status;
        public int Total;
    }
}

WebFormUI.aspx

<%@ Page Language="C#" %>

<%@ Import Namespace="EmailSendingWebApplication" %>

<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        var svc = new EmailService();
        UpdateProgress(svc.GetProgress());
    }

    protected void SendEmailsButton_Click(object sender, EventArgs e)
    {
        // arbitrary params - modify to suit
        string criteria = string.Empty;
        int anotherCriteria = 0;

        var svc = new EmailService();
        UpdateProgress(svc.SendEmails(criteria, anotherCriteria));
    }

    protected void CancelEmailProcessButton_Click(object sender, EventArgs e)
    {
        var svc = new EmailService();
        UpdateProgress(svc.CancelEmailProcess());
    }

    private void UpdateProgress(EmailSendingProgress progress)
    {
        SetButtonState(progress);
        DisplayProgress(progress);
    }
    private void DisplayProgress(EmailSendingProgress progress)
    {
        if (progress != null)
        {
            EmailProcessProgressLabel.Text = string.Format("Sending {0} of {1}", progress.Current, progress.Total);
            EmailProcessStatusLabel.Text = progress.Status.ToString();
            EmailProcessMessageLabel.Text = progress.Message;
        }
        else
        {
            EmailProcessProgressLabel.Text = string.Empty;
            EmailProcessStatusLabel.Text = string.Empty;
            EmailProcessMessageLabel.Text = string.Empty;
        }
    }

    private void SetButtonState(EmailSendingProgress progress)
    {
        if (progress != null &&
            (progress.Status == ProcessState.Starting || progress.Status == ProcessState.Processing))
        {
            CancelEmailProcessButton.Visible = true;
            SendEmailsButton.Visible = false;
        }
        else
        {
            CancelEmailProcessButton.Visible = false;
            SendEmailsButton.Visible = true;
        }
    }

    protected void RefreshButton_Click(object sender, EventArgs e)
    {
        // noop just to get postback. you could also use meta headers to refresh the page automatically 
        // but why?
    }
</script>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
<html xmlns="http://www.w3.org/1999/xhtml"&gt;
<head id="Head1" runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <br />
        EmailProcessStatus:
        <asp:Label ID="EmailProcessStatusLabel" runat="server" Text="EmailProcessStatus"></asp:Label>
        <br />
        EmailProcessProgress:
        <asp:Label ID="EmailProcessProgressLabel" runat="server" Text="EmailProcessProgress"></asp:Label>
        <br />
        EmailProcessMessage:<asp:Label ID="EmailProcessMessageLabel" runat="server" Text="EmailProcessMessage"></asp:Label>
        <br />
        <br />
        <asp:Button ID="SendEmailsButton" runat="server" OnClick="SendEmailsButton_Click"
            Text="Send Emails" />
        &nbsp;<asp:Button ID="CancelEmailProcessButton" runat="server" OnClick="CancelEmailProcessButton_Click"
            Text="Cancel Email Process" />
        <br />
        <br />
        <asp:Button ID="RefreshButton" runat="server" OnClick="RefreshButton_Click" Text="Refresh" />
    </div>
    </form>
</body>
</html>

AjaxUI.htm

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
<html xmlns="http://www.w3.org/1999/xhtml"&gt;
<head>
    <title></title>

    <script type="text/javascript">
        //http://www.codeproject.com/Articles/38999/Consuming-ASP-net-WebServices-WCF-Services-and-sta.aspx

        var ProcessState = ["Idle", "Starting", "Processing", "Cancelled", "Error"];

        function createXHR() {
            var xhr;

            if (window.XMLHttpRequest) {
                xhr = new XMLHttpRequest();
            }
            else if (window.ActiveXObject) {
                xhr = new ActiveXObject('Microsoft.XMLHTTP');
            }
            else {
                throw new Error("Could not create XMLHttpRequest object.");
            }
            return xhr;
        }

        function emailAjax(operation, postData, callback) {

            var xhr = createXHR();

            xhr.open("POST", "EmailService.asmx/" + operation, true);
            xhr.onreadystatechange = function() {
                if (xhr.readyState === 4) {
                    callback(xhr.responseText);
                }
            };
            xhr.setRequestHeader("content-type", "application/json");
            xhr.send(postData);
        }

        function $(id) {
            var e = document.getElementById(id);
            return e;
        }
        function startProcess() {
            var postData = '{"criteria" : "something", "anotherCriteria" : "1"}';
            emailAjax("SendEmails", postData, displayProgress);
        }

        function cancelProcess() {
            emailAjax("CancelEmailProcess", null, displayProgress);
        }

        function getProgress() {
            emailAjax("GetProgress", null, displayProgress);
        }

        function displayProgress(json) {
            eval('var result=' + json + '; var prg=result.d;');

            $("EmailProcessMessage").innerHTML = "";
            $("EmailProcessStatus").innerHTML = "";
            $("EmailProcessProgress").innerHTML = "";
            $("CancelEmailProcessButton").style.display = "none";
            $("SendEmailsButton").style.display = "none";

            if (prg) {
                $("EmailProcessMessage").innerHTML = prg.Message;
                $("EmailProcessStatus").innerHTML = ProcessState[prg.Status];
                $("EmailProcessProgress").innerHTML = "Sending " + prg.Current + " of " + prg.Total;
            }

            if (prg && (prg.Status == 1 || prg.Status == 2)) {
                $("SendEmailsButton").style.display = "none";
                $("CancelEmailProcessButton").style.display = "inline";
            }
            else {
                $("CancelEmailProcessButton").style.display = "none";
                $("SendEmailsButton").style.display = "inline";
            }

        }

        function init() {
            $("SendEmailsButton").onclick = startProcess;
            $("CancelEmailProcessButton").onclick = cancelProcess;
            // kinda quick but we are only proccing 100 emails for demo
            window.setInterval(getProgress, 1000);


        }
    </script>

</head>
<body onload="init()">
    EmailProcessStatus:<span id="EmailProcessStatus"></span><br />
    EmailProcessProgress:<span id="EmailProcessProgress"></span><br />
    EmailProcessMessage:<span id="EmailProcessMessage"></span><br />
    <input type="button" id="SendEmailsButton" value="SendEmails" style="display: none" />
    <input type="button" id="CancelEmailProcessButton" value="CancelEmailProcess" style="display: none" />
</body>
</html>
Sky Sanders
Thanks a lot in advance :)
emre
source for the project is here:http://skysanders.codeplex.com/SourceControl/list/changesets
Sky Sanders
A: 

So user will have to leave the browser window open until all the e-mails are sent? Does not sound very good. I would solve this using a daemon or simple script that is run by cron (and checks db if there is something to send), on Windows I hope you can do something similar (write Windows service etc.). This is a purely server-side task, I think ajaxifying it shows that author of the web app wasn't able to make it in a better way, it may even make your web app to be mentioned on thedailywtf.com :)

Messa
hahaha really? :)
emre
ok, messa, should my app be mentioned in dwtf? ;-)
Sky Sanders