Setting up Hangfire in Episerver

Hangfire is a great tool for running tasks in the background. When working on the Episerver CMS and Commerce projects, you have to send emails or run another background task quite often, and Hangfire helps to achieve this reliably.

Installation

For Episerver integration you will need the main package of Hangfire and also StructureMap integration package:

Install-Package Hangfire
Install-Package Hangfire.StructureMap

Configuration

First, add the base configuration to the Owin startup class:

GlobalConfiguration.Configuration
    .UseSqlServerStorage("EPiServerDB");
app.UseHangfireDashboard();
app.UseHangfireServer();

Here you are configuring the SQL Server connection string. I prefer to use the same DB as for Episerver, so am using Episerver DB connection name. Then we are configuring default dashboard (default path - "/hangfire") and Hangfire server to run in the same ASP.NET application.

Dashboard authorization

By default, Hangfire allows access to the dashboard only for local requests. But I wanted Episerver admins to access it. For this purpose, you can implement a custom authorization filter:

public class AdminAuthorizationFilter : IDashboardAuthorizationFilter
{
    public bool Authorize(DashboardContext context)
    {
        return PrincipalInfo.HasAdminAccess;
    }
}

Here I am using Episerver's PrincipalInfo, and it's property - HasAdminAccess to find out if a user has admin rights.

Then configure dashboard to use it:

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

When configuring additional options, you also have to provide a path to the dashboard.

StructureMap configuration

You have to configure Hangfire to support dependency injection in your background jobs. You can do it in the initializable module where you are setting up your IoC container:

[ModuleDependency(typeof(ServiceContainerInitialization))]
[InitializableModule]
public class DependencyResolverInitialization : IConfigurableModule
{
    public void ConfigureContainer(ServiceConfigurationContext context)
    {
        // IoC configuration here

        Hangfire.GlobalConfiguration.Configuration.UseStructureMapActivator(context.StructureMap());
    }
}

While you can use Hangfire by calling methods from the static BackgroundJob class, I would suggest using IBackgroundJobClient which is injected into your classes. By default, StructureMap is not able to resolve it. So you have to add IBackgroundJobClient to the StructureMap configuration. Here is an example, how to configure it from the StructureMap registry:

For<IBackgroundJobClient>().Singleton().Use(() => new BackgroundJobClient());

Dashboard integration in the Episerver Shell

I found only one solution to achieve it - display Hangfire dashboard in the iframe. For this, you have to create a container page. As I am using MVC, the easiest way was by introducing a controller and Razor view. The controller is very simple:

public class HangfireCmsController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

I have used the name of the controller "HangfireCms" so that it will not collide with dashboard URL.

Now we can add the view:

@using EPiServer
@using EPiServer.Framework.Web.Mvc.Html
@using EPiServer.Framework.Web.Resources
@using EPiServer.Shell.Navigation

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head runat="server">
    <meta name="viewport" content="width=device-width" />
    <title>Dashboard</title>

    @Html.Raw(ClientResources.RenderResources("ShellCore"))
    @Html.Raw(ClientResources.RenderResources("ShellWidgets"))
    @Html.Raw(ClientResources.RenderResources("ShellCoreLightTheme"))
    @Html.Raw(ClientResources.RenderResources("ShellWidgetsLightTheme"))
    @Html.Raw(ClientResources.RenderResources("Navigation"))
    @Html.CssLink(UriSupport.ResolveUrlFromUIBySettings("App_Themes/Default/Styles/system.css"))
    @Html.CssLink(UriSupport.ResolveUrlFromUIBySettings("App_Themes/Default/Styles/ToolButton.css"))
    <style>
        .iframe-container {
            width: 100%;
            height: 100%;
        }
        iframe {
            width:100%;
            height:100%;
        }
    </style>
</head>
<body class="claro">
@Html.Raw(Html.GlobalMenu())
<div class="iframe-container">
    <iframe src="/hangfire" title="Hangfire Dashboard">
        <p>Your browser does not support iframes.</p>
    </iframe>
</div>
</body>
</html>

Now, you can configure Shell menus. Create a menu provider which has a new section for Hangfire and sub-menu which points to the dashboard "wrapper" controller:

[MenuProvider]
public class HangfireMenuProvider : IMenuProvider
{
    public IEnumerable<MenuItem> GetMenuItems()
    {
        var section =
            new SectionMenuItem("Hangfire", "/global/hangfire")
            {
                IsAvailable = request => PrincipalInfo.HasAdminAccess
            };

        var dashboard =
            new UrlMenuItem("Dashboard", "/global/hangfire/dashboard", "/hangfirecms")
            {
                IsAvailable = request => PrincipalInfo.HasAdminAccess
            };
        return new MenuItem[] { section, dashboard };
    }
}

The last step is removing "Back to the site" link on the dashboard as it is not needed. You can achieve it by AppPath property of DashboardOptions to null.

app.UseHangfireDashboard(
    "/hangfire",
    new DashboardOptions
    {
        Authorization = new[] {new AdminAuthorizationFilter()},
        AppPath = null // Hide back to site link
    });

Usage

The simplest case is fire and forget. Inject IBackgroundJobClient in your code where you need it and call Enqueue method to run the task:

public class ReceiptController : Controller
{
    private readonly IBackgroundJobClient _jobClient;
    private readonly EmailClient _emailClient;

    public Events(EmailClient emailClient, IBackgroundJobClient jobClient)
    {
        _jobClient = jobClient ?? throw new ArgumentNullException(nameof(jobClient));
        _emailClient = emailClient ?? throw new ArgumentNullException(nameof(emailClient));
    }

    public ActionResult Index()
    {
        OrderReference orderLink = // obtain a purchase order link ...
        _jobClient.Enqueue(() => _emailClient.SendReceiptEmail(orderLink));
        return View();
    }
}

Make sure that you are passing only simple parameters to the method as those are serialized and stored in the database. Also, make sure that you are not using anything which depends on the web context (request, response, etc.) in the task code. In my example, EmailClient should not depend on HttpContext for instance.

For more examples, see Hangfire documentation.