I have a Parallel.ForEach loop doing the following:
ConcurrentBag<Participant> participantsList = new ConcurrentBag<Participant>() {
new Participant() { EMail = "Test.Mail@outlook.de", FirstName = "First1", LastName = "Last1"},
new Participant() { EMail = "Test.Mail@gmail.de", FirstName = "First2", LastName = "Last2"},
new Participant() { EMail = "Test.Mail@yahoo.de", FirstName = "First3", LastName = "Last3"},
};
Parallel.ForEach(participantsList, (p) =>
{
var mail = new Email("REGISTERMAIL", p.FirstName, p.LastName);
mail.AddRecipient(p.EMail);
mail.Send();
});
What happens is that the ForEach-Loop only processes one participant successfully. The others two throw "Object reference not set to an instance of an object.".
I've first concluded this would be due to the process not being thread safe so I replaced participantsList (first being of type List<>) with a ConcurrentBag. But the error still occurs.
I can't see any other place where a thread would share one collection in my code since every thread gets its own instance if Email.
What could be the source of that error? Maybe static properties inside Email? Since they are not copied over for every instance...
Everything works fine with a normal foreach loop.
EDIT: There was one static dictionary inside the EMail class. I replaced it with a ConcurrentDictionary and that did not fix the problem yet.
Solution:
Thanks to Luke Merrett I found the problem and was able to solve it:
I was referencing HttpContext.Current on multiple threads. Problem is that HttpContext.Current becomes null when you switch the thread. So I had to pass the the current HttpContext.Current into every thread I start:
HttpContext ctx = HttpContext.Current;
Parallel.ForEach(participantsList, (p) =>
{
HttpContext.Current = ctx;
var mail = new Email("REGISTERMAIL", p.FirstName, p.LastName);
mail.AddRecipient(p.EMail);
mail.Send();
});
More information
The Email class is a wrapper around System.Net.Mail. The contructor takes in a creation option which runs a standard email initialisation predefined by the user and stored in a static dictionary. It also takes in an array of objects to apply String.Format on it:
public Email(string creationFlag, params object[] formatObjects)
{
Mail = new MailMessage();
Smtp = new SmtpClient();
Action<Email, object[]> action;
if (OptionsToActionsMap.TryGetValue(creationFlag.ToString(), out action))
{
action(this, formatObjects);
}
}
The following action is then run:
private static Action<Email, object[]> registerMail = new Action<Email, object[]>((mail, formatParams) =>
{
mail.Smtp.Host = "smtp.sendgrid.net";
mail.SetCredentials(WebConfigurationManager.AppSettings["mailAccount"],
WebConfigurationManager.AppSettings["mailPassword"]);
mail.From = "no-reply@sender.de";
mail.Subject = "Deine Anmeldung für die TNTC.";
mail.AddAttachment(HttpContext.Current.Server.MapPath("~/img/TNTC-Logo.png"), "logo", new ContentType("image/png"));
mail.AddAttachment(HttpContext.Current.Server.MapPath("~/img/Anfahrt.png"), "anfahrt", new ContentType("image/png"));
mail.AddHtmlView(HttpContext.Current.Server.MapPath("~/EMail/MailBody.html"), formatParams);
});
Now here there are two methods called namely AddAttachment:
public void AddAttachment(string path, string contentId, ContentType ct)
{
if (LinkedResources == null)
{
LinkedResources = new List<LinkedResource>();
}
var linkedResource = new LinkedResource(path, ct);
linkedResource.ContentId = contentId;
LinkedResources.Add(linkedResource);
}
and AddHtmlView which invokes AddView with a content type of "text/html":
public virtual void AddView(string path, string ctype, params object[] formatObjects)
{
if (Views == null)
{
Views = new List<AlternateView>();
}
if (new ContentType(ctype) != null)
{
var view = AlternateView.CreateAlternateViewFromString(String.Format(File.ReadAllText(path), formatObjects), null, ctype);
Mail.AlternateViews.Add(view);
}
}
Now mail.Send() just adds the linked resources to every view and sends the mail:
public virtual void Send()
{
if (EmailValid())
{
if (LinkedResources.Count() > 0)
{
foreach (var view in Mail.AlternateViews)
{
foreach (var linkedResource in LinkedResources)
{
view.LinkedResources.Add(linkedResource);
}
}
}
foreach (var view in Views)
{
Mail.AlternateViews.Add(view);
}
Smtp.Send(Mail);
}
}