Hangfire – wysyłka email w tle

Wprowadzenie

W poprzednim wpisie pokazałem, jak w aplikacji ASP.NET MVC wysyłać wiadomości e-mail z wykorzystaniem biblioteki Postal. Wspomniałem również, że wysyłka email w ramach żądania HTTP nie jest dobrym pomysłem, że lepiej skorzystać z jakiegoś mechanizmu kolejek oraz ponawiania operacji. Jednym z dostępnych narzędzi jest biblioteka Hangfire, która w prosty sposób może wysłać e-mail w tle, a do tego ponowić operację w momencie wystąpienia błędu.

Hangfire

Ostatnio do kolejkowania zadań oraz wykonywania ich w tle wykorzystuję bibliotekę Hangfire (https://www.hangfire.io/). Lubię ją z kilku powodów:

  • Jest darmowa, więc nie muszę płacić – jest również wersja płatna z kilkoma ciekawymi funkcjonalnościami, ale na ogół ich nie potrzebuję.
  • Bardzo łatwa integracja z aplikacją ASP.NET MVC – w praktyce wystarczy dodać kilka linijek w Startup.cs, aby korzystać z zadań wykonywanych w tle.
  • Fajny dashboard z informacjami o wykonywanych zadaniach.
  • Możliwość wykonywania zadań na różnych komputerach, więc relatywnie łatwo można skalować wykonywanie zadań na kilka maszyn.
  • Jest elastyczna w wykorzystywaniu poszczególnych elementów biblioteki, zadania mogą być wykonywane w zupełnie innej aplikacji niż ta, w której są dodawane, podobnie z dashboardem.
  • Wersja płatna jest relatywnie tania – 500$ za organizację (nie programistę, jak to bywa na ogół) za rok.

Hangfire – instalacja oraz konfiguracja

Standardowo w pierwszej kolejności instalujemy pakiet Hangfire (https://www.nuget.org/packages/HangFire). Domyślnie Hangfire korzysta z bazy danych MS Sql Server do przechowywania informacji o zadaniach. W wersji płatnej możemy skorzystać z Redisa. Jak podają twórcy biblioteki, implementacja z wykorzystaniem Redisa jest około 4 razy szybsza niż wykorzystywanie bazy danych.

W przypadku MS Sql Servera możemy użyć tej samej bazy, w której normalnie przechowujemy dane, lub zdecydować się na dedykowaną bazę dla biblioteki. W przykładzie dodałem connection stringa do web.config:

<connectionStrings>
<add name="DefaultConnection" connectionString="Server=localhost\sqlexpress;Database=PostalAndHangfire;Trusted_Connection=True;" providerName="System.Data.SqlClient" />
</connectionStrings>
view raw web.config hosted with ❤ by GitHub

Mając skonfigurowane źródło przechowywania danych, możemy skonfigurować już samą bibliotekę. Wszystko robi się w pliku Startup.cs:

public class Startup
{
public void Configuration(IAppBuilder app)
{
GlobalConfiguration.Configuration
.UseSqlServerStorage("DefaultConnection");
app.UseHangfireDashboard();
app.UseHangfireServer();
}
}
view raw Startup.cs hosted with ❤ by GitHub

Konfiguracja składa się z wywołania trzech metod:

  • UseSqlServerStorage – informuje, że korzystamy z bazy MS Sql Server do przechowywania informacji oraz przekazuje nazwę connection stringa, którego ma użyć biblioteka.
  • UseHangfireDashboard – informuje, że w aplikacji ma być dostępny dashboard z informacjami o zadaniach.
  • UseHangfireServer – informuje, że w ramach aplikacji mają działać workery, które będą wykonywać zadania.

Jak wspomniałem wcześniej, poszczególne elementy biblioteki możemy zaimplementować w różnych aplikacjach. Możemy na przykład mieć jedną aplikację z dashboardami z wszystkich aplikacji, w której wykorzystujemy Hangfire. Aplikacja może działać tylko w sieci lokalnej firmy i nie ma do niej dostępu z Internetu.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Dodanie zadania do wykonania

Po skonfigurowaniu Hangfire samo dodanie zadania do wykonania jest bardzo proste. Wystarczy zamienić wywołanie właściwej metody (np. SendRegisterEmail z klasy UserMailer) na wywołanie statycznej metody Enqueue z klasy BackgroundJob (są też inne sposoby, ale o tym w późniejszych wpisach), gdzie w parametrze przekazujemy w formie lambdy wcześniejsze wywołanie. Poniżej znajduje się metoda z logiki biznesowej pokazująca dodanie zadania polegającego na wysłaniu wiadomości e-mail. W komentarzu znajduje się również wcześniejsza wersja, dla porównania.

public void Add(User user)
{
if (user == null)
{
throw new ArgumentNullException("user");
}
//walidacja danych
//zapis danych w bazie
//wysyłka maila
//UserMailer.SendRegisterEmail(user);
BackgroundJob.Enqueue(() => UserMailer.SendRegisterEmail(user));
}
view raw UserLogic.cs hosted with ❤ by GitHub

Wykonanie zadania oraz dashboard

Mając już zmieniony kod, możemy spróbować dodać nowego użytkownika, aby system wysłał wiadomość e-mail z wykorzystaniem Hangfire. Po dodaniu zadania pojawi się ono na liście w dashboardzie. Domyślnie dashboard dostępny jest pod adresem [adres aplikacji]/hangfire. W dashboardzie możemy zobaczyć listę zadań z podziałem na statusy (do wykonania, wykonywane, zakończone itp.) czy też możemy zobaczyć szczegóły zadania (zserializowane dane zadania, informacje o ilości wykonywań czy błędy, gdy nie udało się wykonać zadania).

Hangfire lista zadań

Hangfire szczegóły zadania

Hangfire, Postal oraz kontener DI (Autofac)

Na koniec jeszcze jedna istotna informacja na temat integracji Hangfire, Postal oraz kontenera Dependency Injection, w moim przypadku – Autofac. Niestety, kiedy w aplikacji chcecie połączyć te trzy biblioteki (podobnie może być z innymi kontenerami, które wspierają cykl życia obiektu per żądanie HTTP), podczas wykonywania zadania z wysyłką wiadomości otrzymacie błąd, że wymagany jest kontekst HTTP do utworzenia jakiegoś obiektu. Widać to na poniższym zrzucie zadania (co fajne, po poprawieniu błędu zadanie zostało poprawnie wykonane i wiadomość została wysłana w jednej z kolejnych prób, a to pokazuje, że Hangfire działa :)):

Hangfire błąd z zadaniu

Takie działanie aplikacji spowodowane jest tym, że klasa ViewEngineCollection, którą wykorzystujemy w metodzie Send z klasy BaseMailer, w swoim kodzie korzysta z ustawionego w ASP.NET MVC resolvera (czyli w moim przypadku skonfigurowanego kontenera Autofac). Próbuję za jego pomocą wyciągnąć obiekty wewnętrzne z silnika, a one mają ustawiony cykl życia per żądanie. Co ciekawe, tamte obiekty nie są potrzebne do poprawnego wygenerowania treści wiadomości. Jednym z rozwiązań jest stworzenie własnej klasy dziedziczącej po ViewEngineCollection i, w konstruktorze za pomocą refleksji, zmiana wykorzystywanego resolvera, aby klasa nie korzystała z kontenera. Poniżej znajduje się zmieniona klasa BaseMailer, która właśnie wykorzystuje zmienioną klasę ViewEngineCollection. W przypadku gdy nie korzystamy z kontenera, zmiana wcześniejszego kodu nie jest potrzebna.

public class BaseMailer
{
protected void Send(Email email)
{
var mailerName = GetType().Name.Replace("Mailer", string.Empty);
var viewsPath = Path.GetFullPath(string.Format(HostingEnvironment.MapPath(@"~/Views/Emails/{0}"), mailerName));
var engines = new ViewEngineCollectionWithoutResolver();
engines.Add(new FileSystemRazorViewEngine(viewsPath));
var emailService = new EmailService(engines);
emailService.Send(email);
}
private class ViewEngineCollectionWithoutResolver : ViewEngineCollection
{
public ViewEngineCollectionWithoutResolver()
{
var resolverField = typeof(ViewEngineCollection).GetField("_dependencyResolver",
BindingFlags.NonPublic | BindingFlags.Instance);
var resolver = new EmptyResolver();
resolverField.SetValue(this, resolver);
}
private class EmptyResolver : IDependencyResolver
{
public object GetService(Type serviceType)
{
return null;
}
public IEnumerable<object> GetServices(Type serviceType)
{
return Enumerable.Empty<object>();
}
}
}
}
view raw BaseMailer.cs hosted with ❤ by GitHub

Podsumowanie

Dodanie wykonywania zadań z wykorzystaniem kolejek nie musi być bardzo trudne. Dzięki bibliotece Hangfire może być to wręcz łatwe i przyjemne. Jak widać, daje to dużą niezawodność w działaniu aplikacji, poprzez możliwość ponawiania wykonywania zadań, które się nie udały. Dodatkowo przejrzysty dashboard daje dużo informacji o tym, co dzieje się z zadaniami w aplikacji.

Testowy projekt tradycyjnie znajdziesz na moim githubie. Zapraszam do pobrania i przetestowania w praktyce biblioteki Hangfire.

A Tobie jak podoba się Hangfire? Znałeś wcześniej tę bibliotekę? A może używasz czegoś fajniejszego? Daj znać w komentarzu.

 

5 thoughts on “Hangfire – wysyłka email w tle

  • Pingback: dotnetomaniak.pl
  • Hej, jak wygląda to w przypadku gdy IIS wyłączy naszą aplikację po 20 minutach bezczynności?
    Hangfire dalej będzie wykonywać swoje taski?

  • Cześć a kojarzysz może jak sprawdzić czy dane zadanie jest odpalone np: w kontrolerze? Rozumiem, że sam job może dawać informację do kogoś, że jest odpalony ale czy działa to w druga stronę.
    Sprawdzam czy dany job jest odpalony.

    Używam tego od roku. Biblioteka się dobrze sprawuje i jest stabilna. Szkoda tylko, że kliku elementów jej brakuje np: nie ma możliwość skonfigurowania aby job się zrobił się tylko raz. Z poziomu dashboard nie można wyłączyć job-a i z tego co wiem nie ma jak przeglądać historii job-a. Można zobaczyć ostatnie wywołanie ale nie całej historii.

  • Hej, do końca nie rozumiem pytania. Chodzi Ci o to, aby wiedzieć, czy dana metoda, która ma się wykonać jest uruchomiona z kontrolera A lub B? Można chyba przekazać jakieś parametr, który zostanie zserializowany i później to wykorzystać.

    Ja to czasami opakowuje jakieś metody, aby tą somą logikę wykonać w innym kontekście (np. innej kolejce w Hangfire – przykład w demie z prezentacji o Hangfire – https://github.com/danielplawgo/HangfireDemo/blob/master/HangfireDemo/HangfireDemo.Logic/ParserJobs/ParserJobsLogic.cs#L127)

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.