Hangfire - Setup and Test
Introduction
We don't want to have to log into the application and run the 'Send Email' process every day to ensure that all birthday reminders are dispatched. It would be much better if we could use a background task that checked daily for forthcoming birthdays and automatically sent emails. After an extensive search I found 'Hangfire'; this seems to do what we need...
YouTube Video
Installation NuGet Package
Select Tools > NuGet Package Manager > Manage NuGet Packages for Solution and install the following:
- Hangfire.Core
- Hangfire.AspNetCore
- Hangfire.SqlServer
When installing the NuGet packages I noticed that there was a package called simply 'Hangfire'. I hesitated to consider whether this needed to be installed, but I don't think it is (see later).
Hangfire uses a database to record some data and, by default, uses SQL Server - hence Hangfire.SqlServer - but it is possible to store the data in memory. In this case you could install Hangfire.MemoryStorage instead of Hangfire.SqlServer.
The Hangfire documentation for Asp .Net Core applications (https://docs.hangfire.io/en/latest/getting-started/aspnet-core-applications.html) gives an alternative method of installation by editing the .csproj file and adding the following:
<PackageReference Include="Hangfire.Core" Version="1.7.*" />
<PackageReference Include="Hangfire.SqlServer" Version="1.7.*" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.*" />There are two interesting aspects of this. The first is that the list omits just 'Hangfire', so as speculated above, this doesn't seem to be necessary. The second aspect is that [Version="1.7.*"], meaning that the version number is floating and will automatically upgrade to the latest version starting 1.7.
My personal preference is to use the NuGet Package Manager to install the packages, as opposed to modifying the .csproj file, because it avoids errors being displayed when modifying Program.cs. But once installed I can see no reason why .csproj should not be edited to use the floating version number.
Changes to Program.cs
Add this 'using' statement to the top of the file. Note that it is just 'Hangfire', not one of the 'Core' items.
using Hangfire;
using Hangfire.SqlServer;
In the 'builder' section add the following. Most of the documentation and other online sources have the SQL database as a separate 'Hangfire' database. I have chosen the same connection string as the database being used to store Person data, this will result in the Birthdays database also housing the Hangfire tables. My prime reason for doing this is that if I eventually deploy to Azure I want to avoid the cost of an additional database - by keeping to just one database I keep to the 'free' level (at least for the first year).
builder.Services.AddHangfire(x => x
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(builder.Configuration.GetConnectionString("Default"), new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
}));
builder.Services.AddHangfireServer();
I have simply copied the above from the Hangfire documentation and from my initial testing it seems to work without any modification.
As an aside, the first time that the application is run Hangfire populates the database with the required tables.
To provide a way to access the Hangfire dashboard add the following towards the end of the file. (If desired, change the route to suit your situation, e.g. "/hangfire")
app.UseHangfireDashboard("/dashboard");We could access the Hangfire dashboard by manually typing the dashboard address into the browser (e.g. "https://localhost:7270/dashboard"). To make things slightly easier, so that we can view the Hangfire Dashboard from the application, modify NavMenu.razor to insert the following section:
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboard">
<span class="oi oi-list-rich" aria-hidden="true"></span> Hangfire Dashboard
</NavLink>
</div>
Run the application. Nothing much should happen, but in the background the Hangfire tables will be added to the SQL database, and the Hangfire dashboard can also be displayed by selecting the Hangfire menu item.
Hangfire Test
Hangfire can handle a number of different requirements, briefly these are:
- 'Fire and Forget' - typically these are jobs that are only fired once, for example a welcome email when a user signs up for a newsletter. Uses:
- BackgroundJob.Enqueue()
- 'Delayed Job' - essentially the same as a 'Fire & Forget', but is not actioned immediately; maybe a reminder sent a day, or week, after someone signs up for a newsletter offering a discount code. Uses:
- BackgroundJob.Schedule()
- 'Recurring Job' - a job that needs to be repeated on a regular basis. The basis could be more or less any time span, every minute, hour, day, week, etc. For example a report to be sent on the first day of the month to a manager notifying them how many people had signed up for a newsletter. Uses:
- RecurringJob.AddOrUpdate()
- 'Continuous Job' - a job that will run on completion of another job. For example a follow-up email after the newsletter welcome email with a new subscriber's credentials. Uses:
- BackgroundJob.ContinueJobWith()
From the type of available job it seems we will need to use the 'Recurring Job' type to send out reminders about birthdays on a daily basis.
However, before adding the test code I discovered that when adding the EmailService to Program.cs I needed to change the 'AddScoped' to 'AddTransient'
builder.Services.AddTransient<IEmailService, EmailService>(); //Scoped didn't seem to work. Singleton did work.To demonstrate the above job types and to test Hangfire we can add some jobs to Program.cs. Insert the following code into Program.cs before the app.run line, substituting your email address where necsessary.
IEmailService EmailService = app.Services.GetRequiredService<IEmailService>(); //Needed for items below
BackgroundJob.Enqueue(() => EmailService.Send("christopherbell@blazorcode.uk",
"SUBJECT - The program has started",
"This is the body of the email. This is the email to say the program has started"));
BackgroundJob.Schedule(() => EmailService.Send("christopherbell@blazorcode.uk",
"This is sent after a minute"
, "Welcome - This was sent one minute after starting"),
TimeSpan.FromMinutes(1));
RecurringJob.AddOrUpdate(
"Run every 5 minutes", () => EmailService.Send("christopherbell@blazorcode.uk",
"Recurring Email every 5 minutes",
"Another 5 minutes ticks by...."),
"*/5 * * * *");
RecurringJob.AddOrUpdate(
"Run every day at 13:35", () => EmailService.Send("christopherbell@blazorcode.uk",
"Recurring Email every day at 13:35",
"This is the body of the email"),
"35 13 * * *");
This uses the existing EmailService and this requires the code on line 1 to set up the service to be used in Program.cs.
The EmailService takes three parameters, Email address, subject line and body.
- The 'Fire and Forget' job will be sent immediately the program is started and only requires the 3 parameters for the EmailService.
- The 'Scheduled' job requires a TimeSpan parameter, in this case it is set to minutes with a value of 1; this will be sent 1 minute after the program has started.
- The first 'Recurring' job is given an id, which I have simply called "Run every 5 minutes", but could be anything. In addition it is also given a 'Cron' value to describe when the job should be run. (I initially tried TimeSpan.MinuteInterval(5), but this has been deprecated and may not work in future.) The Cron value "*/5 * * * *" will send the email every 5 minutes, but only on 5, 10, 15, 20, etc. minutes past the hour, rather than every 5 minutes after the application actually starts.
- The second 'Recurring' job uses the Cron to send the email daily at 13:35pm.
Run the application and observe the emails you receive.
References:
- Code for this post: Hangfire Setup & Test
- Hangfire: Documentation (Note that this is for .Net 5 - see above for .Net 6)
- Cron: Examples