Параллельно для каждого броска Нет ссылки на исключение объекта

У меня есть цикл Parallel.ForEach, выполняющий следующие действия:

ConcurrentBag<Participant> participantsList = new ConcurrentBag<Participant>() {
    new Participant() { EMail = "[email protected]", FirstName = "First1", LastName = "Last1"},
    new Participant() { EMail = "[email protected]", FirstName = "First2", LastName = "Last2"},
    new Participant() { EMail = "[email protected]", FirstName = "First3", LastName = "Last3"},
};

Parallel.ForEach(participantsList, (p) =>
{
    var mail = new Email("REGISTERMAIL", p.FirstName, p.LastName);
    mail.AddRecipient(p.EMail);
    mail.Send();
});

Что происходит, так это то, что ForEach-Loop успешно обрабатывает только одного участника. Два других выдают «Ссылка на объект не указывает на экземпляр объекта».

Сначала я пришел к выводу, что это может быть связано с тем, что процесс не является потокобезопасным, поэтому я заменил список участников (первый из которых имел тип List‹>) на ConcurrentBag. Но ошибка все равно возникает.

Я не вижу другого места в моем коде, где поток мог бы совместно использовать одну коллекцию, поскольку каждый поток получает свой собственный экземпляр, если Email.

Что может быть источником этой ошибки? Может быть, статические свойства внутри электронной почты? Поскольку они не копируются для каждого экземпляра...

Все работает нормально с обычным циклом foreach.

РЕДАКТИРОВАТЬ: внутри класса EMail был один статический словарь. Я заменил его на ConcurrentDictionary, и это еще не решило проблему.

Решение. Благодаря Люку Мерретту я обнаружил проблему и смог ее решить:
я ссылался на HttpContext.Current в нескольких потоках. Проблема в том, что HttpContext.Current становится нулевым при переключении потока. Поэтому мне пришлось передать текущий HttpContext.Current в каждый поток, который я запускаю:

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

Дополнительная информация

Класс Email является оболочкой для System.Net.Mail. Конструктор принимает параметр создания, который запускает стандартную инициализацию электронной почты, предопределенную пользователем и сохраненную в статическом словаре. Он также принимает массив объектов для применения к нему String.Format:

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

Затем выполняется следующее действие:

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 = "[email protected]";

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

Теперь здесь есть два метода, называемых именно 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);
}

и AddHtmlView, который вызывает AddView с типом содержимого "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);
    }
}

Теперь mail.Send() просто добавляет связанные ресурсы в каждое представление и отправляет почту:

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

person FunkyPeanut    schedule 06.07.2015    source источник
comment
Что такое класс Email в этом контексте, является ли он оберткой вокруг SmtpClient/MailMessage? Изменить: чтобы расширить мой комментарий; Я запускаю этот код локально с издевательскими классами Participant и Email, и никаких исключений не возникает. Сможем ли мы увидеть внутренности этих классов?   -  person Luke Merrett    schedule 06.07.2015
comment
Где именно возникает ошибка? Я скопировал этот код с фиктивными классами участника и электронной почты и не получил никакой ошибки   -  person oppassum    schedule 06.07.2015
comment
Конечно, я предоставлю вам больше информации, просто дайте мне время отредактировать ее :)   -  person FunkyPeanut    schedule 06.07.2015
comment
Пожалуйста, сделайте больше отладки. Если после выполнения совета по повторяющемуся вопросу вы все еще не можете понять это, укажите хороший, минимальный, полный пример кода, достоверно воспроизводящий проблему, а также подробное описание выполненной вами отладки и более конкретное описание проблемы, чем просто констатация того, что NullReferenceException происходит.   -  person Peter Duniho    schedule 06.07.2015
comment
Вы совершенно правы насчет того, что я должен дать более конкретную информацию. Но, потратив ~ один час на отладку, я не смог получить больше информации из этого. Отладчик, например, просто генерирует исключение после выхода из метода mail.Send(). Мне не удалось выяснить, где именно возникает ошибка. Есть ли у вас какие-либо советы по отладке асинхронных операций? Может быть, вам нужны какие-то более конкретные знания там ..   -  person FunkyPeanut    schedule 06.07.2015


Ответы (1)


Я заметил, что вы используете HttpContext в действии, например:

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

Поскольку вы запускаете код асинхронно вне основного потока ответов, HttpContext.Current может быть удалено до завершения обработки (т. е. сервер вернул ответ, поэтому контекста больше нет).

Чтобы обойти это, вам нужно будет передать сопоставленные пути, прежде чем запускать параллельные потоки, или сохранить их, чтобы они не зависели от HttpContext.Current, существующих во время их вызова.

Здесь был опубликован вопрос в этом духе.

person Luke Merrett    schedule 06.07.2015
comment
Привет! Большое спасибо за ваш многообещающий ответ. В качестве быстрого исправления я просто закомментировал строки, используя HttpContext.Current, но, к сожалению, ошибка все еще возникает. - person FunkyPeanut; 06.07.2015
comment
Приветствия для бутона обновления; если бы вы могли добавить трассировку стека, показывающую, где происходит исключение, это помогло бы. - person Luke Merrett; 07.07.2015
comment
Я бы хотел, но трассировка стека не дает никакой полезной информации, поскольку она хранит только около 100 за раз - она ​​полна асинхронных накладных расходов, и ни один вызов (кроме самого последнего) не принадлежит моему собственному коду. Есть ли способы отобразить полную трассировку стека? - person FunkyPeanut; 07.07.2015
comment
Вы сказали, что он показывает последний вызов, он предоставляет номер линии? Или это слишком далеко от фактического исключения? Я не знаю, как получить полную трассировку стека (хотя хотелось бы узнать!) - person Luke Merrett; 07.07.2015
comment
Он просто показал строку фактического кода, который вызывал метод в первую очередь. Но я изменил некоторые настройки отладки, и теперь компилятор выдает мне больше полезной информации. Я постараюсь воспользоваться этим и отчитаюсь о результатах :) - person FunkyPeanut; 07.07.2015
comment
Изменив параметры отладчика, теперь он действительно показывает, где генерируется исключение NullReferenceException! И угадайте, что: вы были правы в своем предположении, что HttpContext.Current имеет значение null. Я сделаю, как вы посоветовали, и отчитаюсь :) - person FunkyPeanut; 07.07.2015
comment
Ради интереса, какой параметр отладчика вы изменили, чтобы улучшить трассировку стека? Рад, что здесь мое предположение подтвердилось! - person Luke Merrett; 07.07.2015
comment
Я проверил параметр «Включить только мой код» в разделе «Отладка» -> «Параметры и настройки» -> «Отладка» -> «Общие». Первоначально я просто хотел, чтобы внешние записи в трассировке стека исчезли. Но после включения этого я каким-то образом получил гораздо более подробные результаты отладчика. - person FunkyPeanut; 07.07.2015
comment
Вы были правы в своем ответе. Я выложу решение выше. Спасибо! - person FunkyPeanut; 07.07.2015