Using Hangfire for scheduled tasks in Umbraco

This post is sparked from a question on Twitter about the retired scheduled tasks option we had in Umbraco v7. It's always a bit of a headscratcher when things you relied on in the past go away, now what? 

As I told Peter here, we've been greatly impressed with Hangfire in the past few years and I would recommend using it after having used it on Our Umbraco for a few years now.

Older versions of Umbraco came with the option to add a scheduledTasks element to the umbracoSettings.config file. It required you to expose a public URL with no security/authentication on it that Umbraco would ping every minute. Not only was this insecure, in order to ping those URLs, a certain number of request needed to come into the site. No traffic meant tasks wouldn't run, making the system quite unreliable.

I know the Umbraco core guys will tell me that we have an excellent built-in BackgroundTaskRunner as well, but that doesn't come with a nice solid dashboard like Hangfire does. The dashboard is super convenient to allow you to manually trigger tasks and do some debugging, it's very handy.

Overview

As a quick overview, here are the moving parts we need to consider when setting up Hangfire for the first time:

  • Make sure we use SQL server (SQL CE is not supported)
  • Install the NuGet package
  • Add a few files to App_Plugins to show the dashboard in the backoffice
  • Make sure to secure the dashboard
  • Set up scheduled tasks (!)
  • Bonus: add messages and progress bars into the dashboards

After the first few things have been set up, creating the next tasks is pretty easy, add a new method and it will run.

Setup

Alright, so you've installed Umbraco using NuGet and you have chosen SQL Server (right?).

Great, I have my favorite setup where I create the Umbraco Web App project and add a class library project added to it. I install the UmbracoCms.Core and UmbracoCms.Web into it.

Next up, installing Hangfire, in your Package Manager Console:

    Install-Package Hangfire
    Install-Package Hangfire.Console

I'll get back to the Hangfire.Console package later.

You will need to install both packages in both your project files in VS. Hangfire adds a few dependencies for you as well. 

Dashboard

Next up: getting a nice dashboard in the backoffice to see what Hangfire is doing, control tasks and see some debug output.

In Umbraco 8, you can create a new dashboard by dropping some files in the App_Plugins folder, I called my plugin "Cultiv.Hangfire", in the ~/App_Plugins/Cultiv.Hangfire/ directory we start with a package.manifest:

    {
        "dashboards":  [
            {
                "alias": "cultiv.Hangfire",
                "view":  "/App_Plugins/Cultiv.Hangfire/dashboard.html",
                "sections":  [ "settings" ]
            }
        ]
    }

This defines the dashboard view (dashboard.html) and tell Umbraco it will go into the Settings section.

At this point a new dashboard will show up in the Settings section and it will be named: [cultiv.Hangfire]

The fact that the name is in square brackets means it's not been translated yet, we can translate it differently for each backoffice user's locale. Let's start with English. We can add a ~/App_Plugins/Cultive.Hangfire/lang/en-US.xml file with the following content:

    <?xml version="1.0" encoding="utf-8" standalone="yes"?>
    <language>
        <area alias="dashboardTabs">
            <key alias="cultiv.Hangfire">Hangfire</key>
        </area>
    </language>

We need to load the Hangfire overview page and the easiest way to do that is through an iframe (feels dirty, works great!):

    <style type="text/css">
        .hangfireWrapper {
            margin: -30px -20px;
        }
    
        .hangfireContent {
            position: absolute;
            width: 100%;
            height: 100%;
        }
    </style>
    
    <div class="hangfireWrapper">
        <iframe name="hangfireIframe" class="hangfireContent" id="Hangfire" frameborder="0" scrolling="yes" marginheight="0" marginwidth="0"
                src="/hangfire/" allowfullscreen="true" style="width:100%!important"
                webkitallowfullscreen="true" mozallowfullscreen="true"
                oallowfullscreen msallowfullscreen="true"></iframe>
    </div>    

Finally, we have to tell initialize Hangfire which can be done through and OwinStartup class. Mine looks like this:

    using Cultiv.Hangfire;
    using Hangfire;
    using Hangfire.Console;
    using Hangfire.SqlServer;
    using Microsoft.Owin;
    using Owin;
    using Umbraco.Web;
    
    [assembly: OwinStartup("UmbracoStandardOwinStartup", typeof(UmbracoStandardOwinStartup))]
    namespace Cultiv.Hangfire
    {
        public class UmbracoStandardOwinStartup : UmbracoDefaultOwinStartup
        {
            public override void Configuration(IAppBuilder app)
            {
                //ensure the default options are configured
                base.Configuration(app);
    
                // Configure hangfire
                var options = new SqlServerStorageOptions { PrepareSchemaIfNecessary = true };
                const string umbracoConnectionName = Umbraco.Core.Constants.System.UmbracoConnectionName;
                var connectionString = System.Configuration
                    .ConfigurationManager
                    .ConnectionStrings[umbracoConnectionName]
                    .ConnectionString;
                
                GlobalConfiguration.Configuration
                    .UseSqlServerStorage(connectionString, options);              
                    
                // Give hangfire a URL and start the server                
                app.UseHangfireDashboard("/hangfire");
                app.UseHangfireServer();
            }
        }
    }

Here, we make sure to set up Hangfire to use SQL Server with the connection string we have for Umbraco in our web.config. Hangfire will create a few tables and track some data in there. Now we can tell Umbraco that we want to use this startup class, we can update the owin:appStartup appSetting in the web.config:

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

We have something that looks a little like this now:

Securing the dashboard

This all great.. but! Now you go to your site URL and add /hangfire and... presto: any unauthenticated user can see and control all the tasks you have scheduled to run. Not ideal, let's fix that.

First off, we can write an authorization filter that will return true if the current logged in user is in the "admin" user group:

    using System.Linq;
    using System.Web;
    using Hangfire.Dashboard;
    using Umbraco.Web.Composing;
    using Umbraco.Web.Security;
    
    namespace Cultiv.Hangfire
    {
        public class UmbracoAuthorizationFilter : IDashboardAuthorizationFilter
        {
            public bool Authorize(DashboardContext context)
            {
                var http = new HttpContextWrapper(HttpContext.Current);
                var ticket = http.GetUmbracoAuthTicket();
                http.AuthenticateCurrentRequest(ticket, true);
    
                var user = Current.UmbracoContext.Security.CurrentUser;
    
                return user != null && user.Groups.Any(g => g.Alias == "admin");
            }
        }
    }

Then we need to tell Hangfire to use it, so when we give it a URL, we also give it some options, update like so:

    var dashboardOptions = new DashboardOptions { Authorization = new[] { new UmbracoAuthorizationFilter() } };
    app.UseHangfireDashboard("/hangfire", dashboardOptions);

All secure! If you go to the /hangfire url now, you'll find that you don't see the dashboard unless you're logged in as an admin.

Schedule tasks

Yay, we're ready to add new tasks that we want to run, basically we can call any code in our codebase. Here's an example:

    using Hangfire;
    using Hangfire.Server;
    
    namespace Cultiv.Hangfire
    {
        public class ScheduleHangfireJobs
        {
            public void DoSomething()
            {
                RecurringJob.AddOrUpdate(() => DoIt(null), Cron.HourInterval(12));
            }
    
            public void DoIt(PerformContext context)
            {
                // Do something!
            }
        }
    }

As you can see we're scheduling this task to run every 12 hours. Right now, we're not doing anything but I'll show some demo code in a minute.

The last thing we need to do is to add it to the UmbracoStandardOwinStartup class, after app.UseHangfireServer();

    // Schedule jobs
    var scheduler = new ScheduleHangfireJobs();
    scheduler.DoSomething();    

Your dashboard should be showing that new task now!

Debugging and monitoring progress

So you have code running every 12 hours now. It would be good to know what's going on. Especially if we have some longer running code, it could even be nice to have a progress bar show up.

This is where the Hangfire.Console package comes in. You can add WriteLine statements just like in a console app. We can also add a progress bar. In our UmbracoStandardOwinStartup we can update the global configuration to use the console package:

    GlobalConfiguration.Configuration
        .UseSqlServerStorage(connectionString, options)
        .UseConsole();    

And then in our DoIt method we can do things like:

    context.WriteLine("Normal text!");

    context.SetTextColor(ConsoleTextColor.Red);
    context.WriteLine("Red text!");
    context.ResetTextColor();

    context.SetTextColor(ConsoleTextColor.Yellow);
    context.WriteLine("Yellow text!");
    context.ResetTextColor();

    // create progress bar
    var progress = context.WriteProgressBar();
    for (var i = 0; i <= 100; i++)
    {
        // update value for previously created progress bar
        progress.SetValue(i);
    }    

The result is probably very predictable and hopefully very useful:

Lovely! Now.. it's up to you, run all the code you need. The dashboard allows you to start each task manually and you can monitor them on the Jobs tab. 

One tip: start slow, don't schedule something to run every 10 seconds until you know it works well otherwise you might be hitting your breakpoints very often. I speak of experience.. ;-)

Sebastiaan Janssen

Dutch guy living in (and loving) Copenhagen, working at Umbraco HQ. Lifehacker, skeptic, music lover, cyclist, developer.