NetCore Tutorials - NetCore 2.2 Console + HangFire + Distributed Processing


The purpose of this fun little guide is to demonstrate how to do three simple things.

  1. Create a HangFire client (this generates work).
  2. Create a HangFire server (this processes work in isolation).
  3. How to integrate with SQL Server.

Now there is plenty of documentation at HangFire.io, but there is just a very heavy focus on Asp.Net / AspNetCore that I needed some additional clarity myself.


Creating The Client Project

  1. Create a new NetCore Console project.
  2. Set all Build Configurations to build in C# (latest minor version.)
  3. Make sure the NetCore 2.2 runtime is selected.

NuGets


Program.cs

using Hangfire;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Threading.Tasks;

namespace HangFire.NetCore.ClientDemo
{
    public class Program
    {
        private static IConfigurationRoot _configuration { get; set; }

        public static async Task Main(string[] args)
        {
            ConfigureApplication();

            await Console.Out.WriteLineAsync("HangFire Client has started. Sending test message...");

            BackgroundJob.Enqueue(() => Console.WriteLine($"Hello TestHost Sever {DateTime.Now}!"));

            await Console.Out.WriteLineAsync("HangFire Client finished its work. Press return to exit...");
            await Console.In.ReadLineAsync();
        }

        private static void ConfigureApplication()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

            _configuration = builder.Build();

            GlobalConfiguration
                .Configuration
                .UseColouredConsoleLogProvider()
                .UseSqlServerStorage(_configuration.GetConnectionString("HangFire"));
        }
    }
}

Client Program Breakdown

The Microsoft extensions packages help us quickly put together a ConfigurationBuild and identify the ConfigurationRoot from the appSettings.json file. This should be more familiar to AspNetCore developers as they have familiarity with these dependencies and IAppBuilder configuration and building. All of that is missing here so we start with barebones.

The Async Main method maybe off putting... but it should not be in 2019. This will throw an error if the project is not at least C# 7.1 (I personally am always running the latest minor version.) One thing to remember when changing this value, is to choose All Configurations before making the change. The setting is configuration specific so if it is set only Debug, Release will be broken.

The lines that are HangFire specific are the BackgroundJob.Enqueue(() => {}) and the GlobalConfiguration. The GlobalConfiguration is setting up HangFire to use a coloured output to console and use SqlServer based on the ConnectionString named "HangFire". We will configure the SqlServer last. The BackgroundJob is setting the future work of Console.WriteLine with a message to be executed by the server.


Summary

We need to create a console application that can now process the work the client creates and read settings from an appsettings.json.


Creating The Server Project

  1. Create a new NetCore Console project.
  2. Set all Build Configurations to build in C# (latest minor version.)
  3. Make sure the NetCore 2.2 runtime is selected.

NuGets


Program.cs

using Hangfire;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Threading.Tasks;

namespace HangFire.NetCore.ServerDemo
{
    public class Program
    {
        private static IConfigurationRoot _configuration { get; set; }
        private static BackgroundJobServer _backgroundJobServer { get; set; }

        public static async Task Main(string[] args)
        {
            AppDomain.CurrentDomain.ProcessExit += new EventHandler(GracefulServerShutdown);

            ConfigureApplication();

            await Console.Out.WriteLineAsync("HangFire Processing Server has started. Press any key to exit...");
            await Console.In.ReadLineAsync();
        }

        private static void ConfigureApplication()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

            _configuration = builder.Build();

            GlobalConfiguration
                .Configuration
                .UseColouredConsoleLogProvider()
                .UseSqlServerStorage(_configuration.GetConnectionString("HangFire"));

            _backgroundJobServer = new BackgroundJobServer();
        }

        private static void GracefulServerShutdown(object sender, EventArgs e)
        {
            _backgroundJobServer.SendStop();
            _backgroundJobServer.Dispose();
        }
    }
}

Server Program Breakdown

You should see the familiar GlobalConfiguration (the client and server wire-up the same way from a configuration standpoint). After the configuration is set, the next thing to pay attention to is the instantiation of the new BackgroundJobServer(). This is our background worker.

A little extra creme is the CurrentDomain.ProcessExit EventHandler being wired up to initiate a graceful shutdown of the BackgroundJobServer


The Magic aka The Thing To Keep In Mind

Both the client and the server need to have either the same framework assembly access or shared library. I highly encourage putting your Fire And Forget code into a NetStandard NuGet Package that can be installed to both projects so when it comes time to instantiate a method from an assembly with parameters, it's there.


The Sql Configuration Nightmare Simplfied

The assumption here is that you are using SqlServer locally installed or at least on a developer box etc. No SqlExpress sillyness here.

First, open up your local SqlServer instance, navigate to Databases, right click the folder and click "New Database". You can call it whatever you want, but I called it HangFire.

Once the "HangFire" database is created, we are going to configure it as a contained database. So right click your new HangFire database and click "New Query". We are going to modify the database, create schema, create user with login password, then authorize user to create tables.

My SQL Server was not configured for contained databases so I included it below commented out if you need to change yours.

--USE [master]
--GO

--EXEC sp_configure 'CONTAINED DATABASE AUTHENTICATION', 1
--GO

--RECONFIGURE
--GO

ALTER DATABASE [HangFire] SET CONTAINMENT = PARTIAL
GO

USE HangFire
GO

CREATE USER [HangFire] WITH PASSWORD = 'HangFireIsAwesome'
GO

IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE [name] = 'HangFire') EXEC ('CREATE SCHEMA [HangFire]')
GO

ALTER AUTHORIZATION ON SCHEMA::[HangFire] TO [HangFire]
GO

GRANT CREATE TABLE TO [HangFire]
GO

Assuming you get no errors and don't need to troubleshoot Sql Server related issues, we are going to verify that the SqlServer allows SQL Server and Windows Authentication mode.

Make note of the user we created above called "HangFire" and the ultra secure password "HangFireIsAwesome".


Alternatively, you could have just made your integrated user (service account) or yourself dbowner and then fired up HangFire with Integrated Security=true; and not had to run any of the above Sql Script.


Now For The AppSettings.Json

I won't be going into setting up multiple environments as it is a little outside the scope of this article. When you add a new Json Configuration file, be sure to check the file properties to "Copy Always" to the output directory or your file will not be found. Make sure both projects have a copy of the file.

{
    "ConnectionStrings": {
        "HangFire": "Data Source=(local);Initial Catalog=HangFire;User Id=HangFire;Password=HangFireIsAwesome;Integrated Security=SSPI;"
    }
}

Alternative Connection String with Integrated Security

{
    "ConnectionStrings": {
        "HangFire": "Data Source=(local);Initial Catalog=HangFire;Integrated Security=true;"
    }
}

Multiple Project Startup

Set both projects to startup together. The Server will be slow on first run as it is creating the appropriate Server Objects it needs.


Client Output

Server Output


Summary

That's pretty much all there is to it. You can now build a distributed processing bot network and take down <Insert Latest Boycotted Company>'s website... or you know offload sending out emails.

You can find the above Solution and Projects hosted at GitHub.


Additional Hangfire Reading

Hangfire.io - Processing Background Jobs
Hangfire.io - Placing Processing Into Another Process
Hangfire.io - Using Sql Server
Hangfire.io - Processing Jobs In A Console Application
Hangfire.io - Configuring The Degree of Parallelism
Hangfire.io - Dealing With Exceptions
Hangfire.io - Best Practices