Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Transakcyjny mailing

W czym problem?

Wysłanie wiadomości e-mail w .NET jest dziecinnie proste:

MailMessage message = new MailMessage(
    "from@server.com", "to@server.com", "Temat", "Treść");
SmtpClient smtp = new SmtpClient();
smtp.Send(message);

Powyższy przykład jest bardzo krótki, choć i tak został napisany niezwykle rozwlekle - wersja zminimalizowana zajęłaby 1 linijkę (w obu przypadkach ustawienia serwera pocztowego znajdują się w pliku konfiguracyjnym). Właściwie ten kawałek kodu powinien wystarczyć nam do szczęścia, ale czasami zdarzają się bardziej skomplikowane przypadki.

Otóż, ostatnio pisałem kod, w którym kilka metod było wykonywanych w jednej transakcji. Jedna z metod wysyłała wiadomość e-mail z powiadomieniem o powodzeniu operacji. Problem pojawiał się, gdy wykonanie kolejnej się nie powiodło. Wówczas, transakcja nie była zatwierdzana - zmiany nie zostały zapisane do bazy, ale wiadomość o powodzeniu była wysyłana! Poniżej zamieściłem przykład takich metod:

public static void CreateUser(string email)
{
    //rejestracja konta użytkownika
    //...
    
    //wysłanie powiadomienia o rejestracji
    MailingHelper.Send(
        new MailMessage("admin@serwis.com",email,
            "Witaj na portalu!", 
            "Twoje konto zostało zarejestrowane."));
}

public static void GiveSiteAccess(string email)
{
    //nadanie uprawnień do serwisu WWW
    //...
}

Oraz ich wywołanie w ramach transakcji:

using (var ts = new TransactionScope())
{
    CreateUser("user@mail.com");
    GiveSiteAccess("user@mail.com");
    
    ts.Complete();
}

Jak widać, gdy wywołanie metody GiveSiteAccess się nie powiedzie, to i tak do użytkownika trafi wiadomość e-mail o sukcesie rejestracji, choć ta de facto nie nastąpiła.

Zadałem, więc sobie pytanie, czy nie da się jakoś podpiąć procesu wysyłania maili do bieżącej transakcji? Okazuje się, że tak i to bardzo łatwo. Wystarczy napisać własny menadżer zasobów i dołączyć go do transakcji.

Rozwiązanie - własny menadżer zasobów

Do informacji o bieżącej transakcji dostajemy się poprzez statyczną właściwość Current klasy System.Transactions.Transaction (w assembly System.Transactions.dll). Tam dostępne są metody, które pozwalają "dopisać się" do bieżącej transakcji:

  • EnlistVolatile - dołącza do transakcji menadżera zasobów ulotnych (zasoby ulotne to np. dane w pamięci; taki menadżer nie obsługuje odtwarzania stanu po błędzie),
  • EnlistPromotableSinglePhase - dołącza do transakcji menadżera zasobów, który obsługuje tryb promotable single phase enlistment,
  • EnlistDurable - dołącza do transakcji menadżera zasobów trwałych (zasoby trwałe to np. dane na dysku twardym; taki menadżer posiada obsługę odtwarzania stanu po błędzie).

Nas będzie interesować pierwsza metoda, gdyż wiadomość e-mail to zasób ulotny. Menadżer zasobów ulotnych w najprostszej postaci to klasa implementująca interfejs IEnlistmentNotification. Nasz menadżer będzie wyglądał następująco:

public class MailSender : IEnlistmentNotification
{
    private readonly MailMessage message;

    public MailSender(MailMessage message)
    {
        this.message = message;
    }

    #region IEnlistmentNotification Members

    public void Commit(Enlistment enlistment)
    {
        //powoduje faktyczne wysłanie wiadomości
        MailingHelper.SendImmediately(message);
        enlistment.Done(); //menadżer zgłasza zakończenie swojej pracy
    }

    public void InDoubt(Enlistment enlistment)
    {
        enlistment.Done();
    }

    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
        //menadżer potwierdza zakończenie przygotowań
        preparingEnlistment.Prepared(); 
    }

    public void Rollback(Enlistment enlistment)
    {
        enlistment.Done(); //menadżer potwierdza wycofanie transakcji
    }

    #endregion
}

Metody interfejsu IEnlistmentNotification będą wykonywane zgodnie z protokołem dwufazowego zatwierdzania (two-phase commit). W dużym skrócie, proces ten składa się z dwóch faz: fazy przygotowania i fazy zatwierdzania. Menadżer transakcji najpierw żąda przygotowania się menadżerów zasobów - metoda Prepare. Każdy menadżer może zgłosić gotowość (wywołanie preparingEnlistment.Prepared()) lub wymusić wycofanie transakcji (wywołanie preparingEnlistment.ForceRollback()). Jeżeli wszystkie menadżery zgłoszą gotować, to transakcja jest zatwierdzana - metoda Commit. W przeciwnym razie wszystkie menadżery dostają polecenie wycofania transakcji - metoda Rollback. Natomiast, gdy menadżer transakcji straci łączność z którymś z menadżerów zasobów, to wywoływana jest metoda InDoubt.

Dołączenie menadżera zasobów do transakcji odbywa się następująco:

Transaction.Current.EnlistVolatile(new MailSender(message), EnlistmentOptions.None);

Ponieważ cała magia jest już znana, oto kod klasy pomocniczej MailingHelper:

public static class MailingHelper
{
    public static void Send(MailMessage message)
    {
        if (Transaction.Current != null)
        {
            Transaction.Current.EnlistVolatile(
                new MailSender(message), EnlistmentOptions.None);
        }
        else
            SendImmediately(message);
    }

    public static void SendImmediately(MailMessage message)
    {
        var smtp = new SmtpClient();
        smtp.EnableSsl = true;
        smtp.Send(message);
    }
}

Jak widać, metoda SendImmediately wysyła wiadomość natychmiast, podczas gdy metoda Send podpina wysłanie wiadomości pod bieżącą transakcję (jeżeli taka istnieje; w przeciwnym razie wysyła maila od razu). Efekt jest taki, jaki chcieliśmy osiągnąć - poczta jest wysyłana dopiero w momencie zatwierdzenia transakcji.

Kilka słów wyjaśnienia

Przedstawione rozwiązanie, wbrew tytułowi, nie jest receptą na zbudowanie w pełni transakcyjnego mailingu. Tutaj tylko osiągamy efekt opóźnienia wysłania wiadomości e-mail do momentu zatwierdzenia transakcji. Ponadto, kod został uproszczony dla zwiększenia czytelności.

Bardziej dociekliwych zapraszam do:

  1. Dokumentacja klasy Transaction.
  2. Dokumentacja klasy TransactionScope.
  3. Dokumentacja interfejsu IEnlistmentNotification.
  4. Artykuł o implementacji własnego menadżera zasobów.
Opublikowane 16 lipca 2008 00:39 przez jakubin
Filed under:

Komentarze:

# Transakcyjny mailing

W czym problem? Wysłanie wiadomości e-mail w .NET jest dziecinnie proste: MailMessage message = new MailMessage

16 lipca 2008 00:58 by Zine.NET

# re: Transakcyjny mailing

fajne :)

16 lipca 2008 02:34 by dario-g

# re: Transakcyjny mailing

... i uzyteczne

16 lipca 2008 10:22 by ucel

# re: Transakcyjny mailing

szkoda tylko ze to nie zadziala na SharePoint :(

Transaction in the .NET are using the DTC

services and Sharepoint OM can't enlist with the services

:(

no ale moze kolejna wersja bedzie to juz wpsierac ;)

Ps.: Tak Biniek, znam Twoja opinie o SharePoint ;)

16 lipca 2008 14:55 by Gutek

# re: Transakcyjny mailing

Prosilbym o wyjasnienie, gdyz moj orzeszek nie do konca pojmuje :)

W jaki sposob jest to lepsze rozwiazanie od zwyklego sprawdzenia, czy wszystko sie powiodlo (np. weryfikujac wpisy w bazie) i dopiero po tym wyslania mejla z potwierdzeniem rejestracji?

Chodzi mi o to, ze nie potrafie wymyslic sobie scenariusza, w ktorym to rozwiazanie faktycznie by bylo ulatwieniem i lepszym rozwiazaniem. Nie chodzi mi o to, ze chce po nim jechac, bo jest bardzo ciekawe i nie taki jest moj zamiar, tylko o to, ze chcialbym to podejscie lepiej zrozumiec. :)

20 lipca 2008 15:37 by M

# re: Transakcyjny mailing

@M: Już śpieszę z wyjaśnieniem :).

Zobacz, w tym wpisie jest metoda CreateUser, która m.in. wysyła maila. Jak twórca tej metody, nie wiesz, kiedy i w jakim kontekscie ktoś ją wywoła. Przypuśćmy, że programista X postanowił ją wykorzystać:

using (var ts = new TransactionScope())

{

   CreateUser(...);

   var ok = DoSomethingElse();

   if (ok)

       ts.Complete();

}

Przy moim podejściu przy braku commitu (bo nie powiodło się DoSomethingElse), mail nie zostanie wysłany. Ba, nawet programista Y nie musi wiedzieć, czy CreateUser wysyła jakieś maile czy nie.

A teraz pytanie, czy stosując weryfikację wpisów w bazie (jakkolwiek sobie to wyobrażasz) da się osiągnąć podobny skutek?

Jedynym sensownym wyjściem, jakie przychodzi mi do głowy, to żeby każda metoda, która wysyła maila, zwracała te maile, a obowiązkiem programisty, byłoby zadbanie, żeby w odpowiednim momencie je wysłać... auuu.

A do tego transakcje mogą być zagnieżdżone.

21 lipca 2008 09:11 by jakubin

# re: Transakcyjny mailing

Dzieki wielkie. Nie wzialem pod uwage, ze kilka osob moze ze mna to pisac. Teraz juz wszystko jest dla mnie jasne.

Dzieki za wypowiedz.

26 lipca 2008 13:07 by M

# re: Transakcyjny mailing

Zareklamowałeś się całkiem skutecznie :) Artykuł równie ciekawy, a pomysł przedstawiony całkiem sprytny.

20 sierpnia 2009 11:57 by Michał Jaskolski

# Transakcyjny mailing

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

10 listopada 2009 12:48 by dotnetomaniak.pl

# re: Transakcyjny mailing

Gdyby ktoś miał problemy z tym rozwiązaniem to warto sprawdzić czy wszystkie readery w transaction scope są zamknięte :)

24 lutego 2015 12:35 by M.R.
Komentarze anonimowe wyłączone

About jakubin

MVP w kategorii C#, MCP. Aktualnie pracuje w Webstruments.pl jako programista C#.