Configuring Hangfire with F#

Laimonas Simutis
2 min readSep 17, 2024

--

I’m writing this blog post in case someone else runs into similar issues. What started as a simple “add Hangfire” task on my to-do list turned into several loops of yak shaving and wasted time.

What is Hangfire

Hangfire provides “an easy way to perform background processing in .NET applications”. Think of it as a tool for scheduled jobs that run in the background as part of your app deployment.

The Issue

The configuration code is very easy to use, and I’ve been using it successfully in one of my web apps where the web portion is written in C#. However, issues arose when I tried to use Hangfire for my other web app that’s fully written in F#.

The issue is simple. Here’s an example where I wanted to create a recurring job that runs every day at 4 PM:

RecurringJob.AddOrUpdate<Services>(
recurringJobId="fullrun",
methodCall=(fun (s:Services) -> s.FullRun()),
cronExpression=Cron.Daily(16, 00),
options=rjo
)

If you try to compile this, you’ll get a cryptic compilation error:

No overloads match for method 'AddOrUpdate'.

Known types of arguments: recurringJobId: string * methodCall: (unit -> Task<unit>) * cronExpression: string

Available overloads:
- RecurringJob.AddOrUpdate(recurringJobId: string, methodCall: Expression<Action>, cronExpression: Func<string>) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate(recurringJobId: string, methodCall: Expression<Action>, cronExpression: Func<string>, ?timeZone: TimeZoneInfo, ?queue: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate(recurringJobId: string, methodCall: Expression<Action>, cronExpression: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate(recurringJobId: string, methodCall: Expression<Action>, cronExpression: string, ?timeZone: TimeZoneInfo, ?queue: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate(recurringJobId: string, methodCall: Expression<Func<Task>>, cronExpression: Func<string>) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate(recurringJobId: string, methodCall: Expression<Func<Task>>, cronExpression: Func<string>, ?timeZone: TimeZoneInfo, ?queue: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate(recurringJobId: string, methodCall: Expression<Func<Task>>, cronExpression: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate(recurringJobId: string, methodCall: Expression<Func<Task>>, cronExpression: string, ?timeZone: TimeZoneInfo, ?queue: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate<'T>(recurringJobId: string, methodCall: Expression<Action<'T>>, cronExpression: Func<string>) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate<'T>(recurringJobId: string, methodCall: Expression<Action<'T>>, cronExpression: Func<string>, ?timeZone: TimeZoneInfo, ?queue: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate<'T>(recurringJobId: string, methodCall: Expression<Action<'T>>, cronExpression: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate<'T>(recurringJobId: string, methodCall: Expression<Action<'T>>, cronExpression: string, ?timeZone: TimeZoneInfo, ?queue: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate<'T>(recurringJobId: string, methodCall: Expression<Func<'T,Task>>, cronExpression: Func<string>) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate<'T>(recurringJobId: string, methodCall: Expression<Func<'T,Task>>, cronExpression: Func<string>, ?timeZone: TimeZoneInfo, ?queue: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate<'T>(recurringJobId: string, methodCall: Expression<Func<'T,Task>>, cronExpression: string) : unit // Argument 'methodCall' doesn't match
- RecurringJob.AddOrUpdate<'T>(recurringJobId: string, methodCall: Expression<Func<'T,Task>>, cronExpression: string, ?timeZone: TimeZoneInfo, ?queue: string) : unit // Argument

The Solution

It seems that the methodCall parameter is what the compiler doesn't like. The solution is painfully simple: cast the return of s.FullRun() (which was returning Task<unit>) to Task:

RecurringJob.AddOrUpdate(
recurringJobId="fullrun",
methodCall=(fun () -> service.SomeBackgroundWork() :> Task),
cronExpression=Cron.Daily(17, 00)
) |> ignore

Adding :> Task does the trick. Now everything compiles and works as expected.

Silly stuff. There may be different ways to approach this, but this solution got me past the issue and running. Hopefully, this post will save someone else some time in the future.

--

--

No responses yet