Hangfire mit Umbraco verwenden

Donnerstag, 30.04.2020

Sören Deger

Mit Hangfire können Tasks gestartet werden, die unabhängig vom Request ablaufen, der die Task gestartet hat. Damit können Aufgaben zeitverzögert gestartet werden und sie überleben einen eventuellen Neustart der Web-Anwendung im IIS.

Wenn man sich im CMS Umbraco mit sogenannten „Background Jobs“ beschäftigen muss, stellt sich zunächst die Frage, wie man so etwas überhaupt implementieren kann. Für unsere Überlegungen gehen wir von folgender Anforderung aus:

Durch einen API-Controller wird eine Datei in einem eigens dafür vorhandenen Ordner (z.B. /App_Data/temp) erstellt und der Link zu dieser Datei per E-Mail versendet. Der Link soll zwei Stunden lang gültig sein und die Datei danach automatisch gelöscht werden.

Die Lösung mittels der Methode Task.Delay() ist hierbei nicht zielführend, weil bei einem Neustart der Anwendung die Task hart beendet wird. Damit bleibt die zu löschende Datei für immer in dem Verzeichnis liegen.

Es gibt eine Technik, mit der man die Tasks beim Hosting-Environment anmelden kann, sodass die Task vor dem Neustart zu Ende ausgeführt wird. Aber mit einem Delay von 2 Stunden ist das nicht machbar, weil die Prozesse normalerweise nur wenige Minuten zur Beendigung ihrer Aufgaben haben.

Auftritt Hangfire

Eine hervorragende Lösung für diese Aufgabe ist das open-source Framework Hangfire (https://www.hangfire.io/), welches in .NET und in .NET Core-Anwendungen sehr einfach und schnell eingesetzt werden kann.

Das Charmante an Hangfire ist, dass weder ein Windows Service, noch irgendein separater Prozess benötigt wird. Ein Background Job ist nichts anderes als eine normale .NET-Methode, für deren Ausführung die notwendigen Metadaten durch Hangfire in einem persistenten Speicher abgelegt werden. So ist sichergestellt, dass ein Background Job ausgeführt werden kann, auch wenn zum Beispiel zwischenzeitlich der Anwendungspool der Webanwendung neu startet. Als persistente Speicher können SQL Server, Redis, PostgreSQL, MongoDB und einige mehr verwendet werden.

Betrachten wir nun die grundsätzliche Implementierung im CMS Umbraco (Version 8).

Zunächst installieren wir in Visual Studio innerhalb unseres Umbraco-Projekts mittels NuGet-Paket-Manager die folgenden beiden Pakete:

Hangfire Nuget
Hangfire mit Nuget installieren

Wir fügen nun eine neue C#-Klasse „Startup.cs“ hinzu:

using System;
using System.Collections.Generic;
using UmbracoWebsite.Classes.Shop.Hangfire;
using Hangfire;
using Hangfire.SqlServer;
using Microsoft.Owin;
using Owin;
using Umbraco.Web;

[assembly: OwinStartup("UmbracoWebsiteStartup", typeof(UmbracoWebsite.Startup))]
namespace UmbracoWebsite
{
    public class Startup : UmbracoDefaultOwinStartup
    {
        private IEnumerable<IDisposable> GetHangfireServers()
        {
            GlobalConfiguration.Configuration
                .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                .UseSimpleAssemblyNameTypeSerializer()
                .UseRecommendedSerializerSettings()
                .UseSqlServerStorage("umbracoDbDSN", new SqlServerStorageOptions
                {
                    CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                    SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                    QueuePollInterval = TimeSpan.Zero,
                    UseRecommendedIsolationLevel = true,
                    UsePageLocksOnDequeue = true,
                    DisableGlobalLocks = true,
                });

            yield return new BackgroundJobServer();
        }

        public override void Configuration(IAppBuilder app)
        {
            base.Configuration(app);
            app.UseHangfireAspNet(GetHangfireServers);

            app.UseHangfireDashboard("/hangfire");
        }
    }
}

	

Diese Klasse erbt von UmbracoDefaultOwinStartup und erlaubt uns, ähnlich wie in .NET Core, zusätzliche Komponenten beim Startup zu registrieren. Wir überschreiben hierzu die Methode „Configuration(IAppBuilder app)“, nutzen die vorhandene Konfiguration unserer Umbraco-App und registrieren zusätzlich HangfireAspNet und das HangfireDashboard.

In der Methode „GetHangfireServer“ definieren wir den persistenten Speicher und verwenden den SqlServerStorage, indem wir den Namen des ConnectionStrings unserer Umbraco-Datenbank „umbracoDbDSN“ eintragen. Hangfire prüft beim Anwendungsstart zunächst, ob es in der entsprechenden SQL-Datenbank die notwendigen Hangfire-Tabellen gibt und erstellt diese, sofern noch nicht vorhanden, einfach neu.

Damit jedoch das Ganze funktioniert und auch unsere Startup.cs beim Anwendungsstart verwendet werden kann, müssen wir in unserer web.config noch den value zum Key „owin:appStartup“ wie folgt anpassen:

<add key="owin:appStartup" value="UmbracoWebsiteStartup" />
	

Damit wir im Kontext unserer Umbraco-Anwendung das Hangfire-Dashboard aufrufen können, fügen wir in der web.config im Key „umbracoReservePaths“ noch den Value „~/hangfire“ hinzu:

<add key="umbracoReservedPaths" value="~/umbraco,~/hangfire" />
	

Nun können wir die Solution neu erstellen und unsere Umbraco-Anwendung sollte fehlerfrei starten.

Wenn wir nun im Browser die URL http://[umbraco-hostname]/hangfire“ aufrufen, öffnet sich das Hangfire-Dashboard:

Hangfire Dashboard
Hangfire Dashboard

Erstellen von Background Jobs

Nachdem die Basis-Implementierung abgeschlossen ist, können wir uns jetzt dem Erstellen von Background Jobs widmen – unter Berücksichtigung der zu Beginn genannten Anforderungen. Zur Erinnerung: Eine Datei soll nach 2 Stunden automatisch gelöscht werden.

Da diese Jobs nichts anderes als normale .NET-Methoden sind, erstellen wir uns eine neue Klasse „BackendResourceJobs.cs“ mit einer Methode „SafeDelete()“:

using System.ComponentModel;
using System.IO;

namespace UmbracoWebsite.Classes.Shop.Hangfire
{
    public class BackendResourceJobs
    {
        [DisplayName("{1} -> SafeDelete()")]
        public void SafeDelete(string file, string displayName)
        {
            try
            {
                if (File.Exists(file))
                    File.Delete(file);
            }
            catch
            {
            }
        }
    }
}

	

Die Methode enthält als ersten Parameter den Dateipfad einer zu löschenden Datei und als zweiten Parameter den Anzeigenamen des Background Jobs, welchen wir im Dashboard nach dem Start des Jobs angezeigt bekommen. Den Anzeigenamen setzen wir im „DisplayName“-Attribute über unserer Methode dynamisch zusammen.

Innerhalb der Methode prüfen wir, ob die Datei wirklich existiert und löschen diese dann.

Den Job starten

Als nächstes müssen wir in einer anderen Klasse, idealerweise die, in welcher wir die zu löschende Datei erstellt haben, diese Methode als Background Job zuweisen und Hangfire mitteilen, wann diese Methode aufgerufen werden soll.

Dies erfolgt mit folgender simpler Anweisung:

BackgroundJob.Schedule<BackendResourceJobs>(x => x.SafeDelete(file, 
"BestellungAuszug"), TimeSpan.FromMinutes(120));

Nachdem wir die Methode zum Erstellen der Datei aufgerufen haben und der Background Job in Hangfire hinzugefügt wurde, sehen wir den Job in unserem Dashboard in der Rubrik „Jobs“ und dort unter „Scheduled“:

Scheduled Jobs
Scheduled Jobs

Wir können uns dort per Klick auf die Id des Jobs weitere Details zum Job ansehen, diesen bei Bedarf löschen oder direkt ausführen lassen. Im linken Bereich des Dashboards befinden sich die Ansichten für fehlerhafte, erfolgreiche und in Bearbeitung befindliche Jobs, sowie für weitere mögliche Zustände eines Jobs.

Man hat hier also immer die vollständige Übersicht aller Jobs.

Natürlich gibt es auch noch viele weitere Möglichkeiten, Jobs zu verwenden. So gibt es neben „Schedule“ auch noch die Methode „Enqueue“ zum sofortigen Übersenden in eine Warteschlange und einige weitere nützliche Funktionalitäten, welche in der offiziellen Dokumentation von Hangfire sehr gut beschrieben sind: https://docs.hangfire.io/en/latest/

Zugriffssicherheit

Nun sollte man aber noch hinterfragen, ob es sinnvoll ist, das Dashboard über /hangfire aufrufen zu können. Hangfire sieht vor, dass das Dashboard nur über localhost aufrufbar ist, was eine rudimentäre Sicherheit bietet, wenn man das so nennen will.

Um dieses Dashboard vernünftig abzusichern, können wir einen eigenen Authorization-Filter entwickeln und diesen beim Startup registrieren. Die Idee dabei ist, dass man das Hangfire-Dashboard nur aufrufen kann, wenn man im Umbraco-Backend angemeldet ist und zur Benutzergruppe „Administrator“ gehört.

In Umbraco fügen wir dazu eine neue Klasse „HangfireAuthorizationFilter.cs“ hinzu:

using Hangfire.Dashboard;
using System.Linq;
using System.Web;
using Umbraco.Core.Security;

namespace UmbracoWebsite.Classes.Shop.Hangfire
{
    public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
    {
        public bool Authorize(DashboardContext context)
        {

            var userService = Umbraco.Core.Composing.Current.Services.UserService;
            var backendUserName = new HttpContextWrapper(HttpContext.Current).GetUmbracoAuthTicket()?.Identity.Name;
            var backendUser = userService.GetByUsername(backendUserName);

            if (backendUser != null && backendUser.Groups.Any(x => x.Alias == "admin"))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
    }
}

	

Unsere Klasse erbt von IDashboardAuthorizationFilter und in unserer Authorize-Methode beziehen wir per UserService den aktuellen Umbraco-Benutzer und geben true zurück, sofern ein Benutzer vorhanden und zur Benutzergruppe „Administrator“ gehört. Ansonsten geben wir false zurück, wodurch der Zugang zum Dashboard verweigert wird.

Damit dies auch von Hangfire berücksichtigt wird, müssen wir den Filter in der Startup.cs bei der HangfireDashboard-Registrierung noch als Dashboard-Option wie folgt übergeben:

public override void Configuration(IAppBuilder app)
        {
            base.Configuration(app);
            app.UseHangfireAspNet(GetHangfireServers);
            
            var options = new DashboardOptions
            {
                Authorization = new[] { new HangfireAuthorizationFilter() }
            };

            app.UseHangfireDashboard("/hangfire", options);
        }

	

Und fertig ist unsere Authentifizierung für das Dashboard.

Fazit

Wie wir sehen, lässt sich also mit sehr wenig Aufwand eine hervorragende und zuverlässige Background-Job-Verwaltung implementieren und dank eigener AuthorizationFilter auch die Zugangsberechtigungen individuell festlegen. Wie in diesem Beispiel können also auch vorhandene Membership-Provider einfach mit angebunden werden. 

In der offiziellen Doku von Hangfire (https://docs.hangfire.io/en/latest) findet man noch weitere nützliche Informationen zu Methoden, Tutorials und Best Practice-Tips, welche man zur Implementierung von Background-Jobs gebrauchen kann. 

Es gibt neben der kostenlosen Version von Hangfire auch noch eine kostenpflichtige Pro-Version, welche zusätzliche Methoden zur Batchverarbeitung von Background-Jobs enthält. 

 

Einen Kommentar verfassen

Anleitung zur Textformatierung

Zum Formatieren des Textes verwenden Sie [b][/b] und [i][/i]. Verwenden Sie [url=http://ihre-site]Text[/url] für Links.

* Pflichtfelder