Send multiple individualised emails via a limited number of SMTP clients This is my first foray into multi-threading, and I'm keen to know if I have fallen into any traps or have room for optimisation/improvement.

Example usage:


var success = new ConcurrentBag&lt;CourseParticipant&gt;();
var fail = new ConcurrentBag&lt;CourseParticipant&gt;();
using (var parallelEmails = new ParallelSmtpEmails())
{
     foreach (var cp in courseParticipants)
     {
        var mail = new MailMessage{
            From = new MailAddress("from@example.com"),
            To = cp.email,
            Subject = "come and attend course",
            BodyFormat = MailFormat.Html
        }
        string token = await GetRegistrationTokenAsync(cp);
        mail.Body = $"&lt;div&gt;&lt;a href='example.com/register?userToken='{token}'&gt;let me come&lt;/a&gt;&lt;/div&gt;"
        parallelEmails.Send(mail, s=&gt; 
        {
            if (s == null)
            {
                success.Add(cp);
            }
            else
            {
                fail.Add(cp);
            }
            mail.Dispose();
        });
     }
     await parallelEmails.SendingComplete();
}



In the code below, I am particularly wondering about the RunWorker method, the do while loop, and if workList.TryGet. If there is nothing in the worklist, this thread will remain looping until CompletedAdding is called or more emails are added. Is this looping an acceptable way to wait the thread?

Being an amateur, I am also grateful for any other code improvements or style points.

public class ParallelSmtpEmails : IDisposable
{
    BlockingCollection&lt;MailTask&gt; _workList;
    List&lt;Task&gt; _tasks;
    int _maxClientCount;

    public ParallelSmtpEmails(int maxSmtpClients = 5)
    {
         //removing the boundedCapacity := maxSmtpClients from BlockingCollection instantiator - the collection can be filled up without blocking
        _workList = new BlockingCollection&lt;MailTask&gt;(new ConcurrentQueue&lt;MailTask&gt;()); 
        _tasks = new List&lt;Task&gt;(maxSmtpClients);
        _maxClientCount = maxSmtpClients;        
    }

    public void CompletedAdding()
    {
        _workList.CompleteAdding();
    }

    public void Send(MailMessage message)
    {
        Send(message, null);
    }

    public void Send(MailMessage message, Action&lt;SmtpException&gt; onComplete)
    {
        _workList.Add(new MailTask() { Message = message, OnComplete = onComplete });
        if (_tasks.Count &lt; _maxClientCount &amp;&amp; _workList.Count &gt; 0)
        {
            _tasks.Add(Task.Factory.StartNew(RunWorker));
        }
    }

    public Task SendingComplete()
    {
        CompletedAdding();
        return Task.WhenAll(_tasks);
    }

    public void RunWorker()
    {
        using (var client = new SmtpClient())
        {
            do
            {
                MailTask m;
                if (_workList.TryTake(out m))
                {
                    try
                    {
                        client.Send(m.Message);
                        m.OnComplete?.Invoke(null);
                    }
                    catch (SmtpException ex)
                    {
                        m.OnComplete?.Invoke(ex);
                    }
                }
            } while (!_workList.IsCompleted);
        }
    }

    #region IDisposable
    bool _disposed;
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~ParallelSmtpEmails()
    {
        Dispose(false);
    }

    void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            // free other managed objects that implement
            // IDisposable only
            _workList.Dispose();
        }

        // release any unmanaged objects
        // set the object references to null

        _disposed = true;
    }
    #endregion //IDisposable

    private class MailTask
    {
        public MailMessage Message { get; set; }
        public Action&lt;SmtpException&gt; OnComplete { get; set; }
    }
}
Don't use SmptClient.Send(), use SmptClient.SendMailAsync() instead. It returns a Task..

Actually, I don't think you need this class at all.

(I'm typing in a notepad, so this is untested and just to illustrate the concept.)

var mailTasks = new List&lt;Task&gt;();
var client = new SmtpClient();

foreach(var participant in courseParticipants)
{
    var mail = new MailMessage
    {
        From = new MailAddress("from@example.com"),
        To = participant.email,
        Subject = "come and attend course",
        BodyFormat = MailFormat.Html
    }

    string token = await GetRegistrationTokenAsync(cp);
    mail.Body = $"&lt;div&gt;&lt;a href='example.com/register?userToken='{token}'&gt;let me come&lt;/a&gt;&lt;/div&gt;";

    mailTasks.Add(client.SendMailAsync(mail));
}

try
{
    await Task.WhenAll(mailTasks);
}
catch(AggregateException ex)
{
    //handle failures
}


This code is much simpler to understand, and therefore maintain.



One last thing before I go: Are you absolutely certain that GetRegistrationTokenAsync() will always return a url safe string?


string token = await GetRegistrationTokenAsync(cp);
mail.Body = $"&lt;div&gt;&lt;a href='example.com/register?userToken='{token}'&gt;let me come&lt;/a&gt;&lt;/div&gt;";



You should probably be Url encoding the token.