Creating Azure WebJobs in F#

My colleague Brian recently wrote about Azure Functions in F#. Azure Functions are great, and I definitely recommend them if they fit your use case.

These functions are built on top of an older background processing system called WebJobs. While Functions have largely eclipsed WebJobs, there remain certain situations where the latter is still a better fit. For example, I recently found myself writing a continuous singleton job, invoking functions programmatically via the JobHost. (Neither of these is possible today in Azure Functions.)

Functions are getting a lot of investment from Microsoft, with recent frameworks and tooling, but the developer experience for WebJobs has been stagnant for a few years. With a little effort, we can drag WebJobs into 2018. Here’s how I’m building and deploying an F# WebJob.

WebJobs Today

When I refer to a “WebJob Project,” I’m describing a .NET console app which uses the WebJobs SDK and is deployed to Azure App Service. Visual Studio offers a project template for this: It scaffolds a simple Hello World project which can be deployed from the Visual Studio UI.

Unfortunately, the template is aging and suffers from a couple of limitations:

  • It’s a legacy .csproj. I prefer the modern, much slimmer sdk-style project files supported by VS2017.
  • Only C# is available. My business logic lives in F# class libraries, and I’d prefer to avoid mixing languages.

Let’s address these!

Hand-Rolling a Modern Alternative

Create a project

First, create a new F# console app. You could do this within Visual Studio, but I prefer dotnet cli:

dotnet new console --target-framework-override net471 --language f#

(Note that we’re targeting NET Framework, as WebJobs do not yet support .NET Core.)

Now we have an F# Hello World. Next, add a couple of packages:

dotnet add package microsoft.azure.webjobs dotnet add package microsoft.azure.webjobs.extensions

And replace the Program.fs source with this example:


open System
open Microsoft.Azure.WebJobs
open Microsoft.Azure.WebJobs.Host
open Microsoft.Azure.WebJobs.Extensions.Timers

let HelloTimer ( [<TimerTrigger("0 */1 * * * *")>] timerInfo : TimerInfo) (log : TraceWriter) =
    log.Info("Hello from F# webjob!");

[<EntryPoint>]
let main argv =
    let config = new JobHostConfiguration()
    config.UseTimers()

    let host = new JobHost(config)
    host.RunAndBlock()

    0

Add it to your solution with e.g. dotnet sln add my-webjob/my-webjob.fsproj.

Deploy it with a web app

From an ASP project in Visual Studio, you can right-click and add an existing console app as a WebJob:

This associates the two projects and makes changes related to packaging and deployment. Unfortunately, it doesn’t work with F# projects. If we wire it up manually, though, it will build and deploy just fine. Here’s how to manually associate an F# console app with an ASP project:

  1. In your web project, create a Properties/webjobs-list.json that looks like this:

    
    {
    "$schema": "http://schemastore.org/schemas/json/webjobs-list.json",
    "WebJobs": [{ "filePath": "../my-webjob/my-webjob.fsproj" }]
    }
    
  2. Reference it from an <ItemGroup> in your web project’s .csproj with <Content Include="Properties/webjobs-list.json" />.
  3. Similarly, in your WebJob project, create a Properties/webjob-publish-settings.json that looks like this:

    
    {
      "$schema": "http://schemastore.org/schemas/json/webjob-publish-settings.json",
      "webJobName": "my-webjob",
      "startTime": null,
      "endTime": null,
      "jobRecurrenceFrequency": null,
      "interval": null,
      "runMode": "Continuous"
    }
    
  4. Reference it from the WebJob’s .fsproj with <None Include="Properties/webjob-publish-settings.json" />
  5. While you’re in there, add the Microsoft.Web.WebJobs.Publish package and its build target:

    
      <ItemGroup>
        <PackageReference Include="microsoft.web.webjobs.publish" Version="2.0.0" />
      </ItemGroup>

    <Import Project="$(NuGetPackageRoot)Microsoft.Web.WebJobs.Publish\2.0.0\tools\webjobs.targets" Condition="Exists('$(NuGetPackageRoot)Microsoft.Web.WebJobs.Publish\2.0.0\tools\webjobs.targets')" />

  6. …Voila! Now when you right-click publish the web app, the WebJob will be deployed along with it.

    These associations are also convenient for logically grouping WebJobs. We keep several WebJobs in one empty “WebJob Container” ASP app, in order to deploy them with one CI step.

    Deploy it alone

    The one thing our sdk-based WebJob project still lacks, compared to the out-of-the-box WebJob template, is the option to right-click and Publish as Azure WebJob:

    I rarely need it since most of our deployments happen in CI, but deployment from your workstation can be helpful when you’re experimenting and iterating on something new.

    I went hunting for alternative deployment tools to shore up this gap. After trying a few approaches, I settled on a small FAKE script. The full script is in the example project linked below, but here’s the deploy task (using FAKE’s Zip and Kudu modules):

    
    
    Target.create "Deploy" (fun _ ->
      let url = new System.Uri(appServiceUrl)  
      let files = !! "bin/Debug/net471/publish/**/*"
      Trace.log (sprintf "zipping %d files" (files |> Seq.toList |> List.length))
      [ @"app_data\jobs\continuous\my-webjob", files ] |> Fake.IO.Zip.zipOfIncludes "out.zip"
      let deployParams :Kudu.ZipDeployParams = {
                                                  PackageLocation="out.zip"
                                                  UserName = publishProfileUser
                                                  Password = publishProfilePassword
                                                  Url=url
                                               }
      Fake.Azure.Kudu.zipDeploy deployParams
    )
    

    After filling in the deployment credentials, the WebJob can be deployed with fake build -t deploy.

    Conclusion

    I hope the WebJobs tooling gets attention soon, but in the meantime, it’s not too hard to build and deploy a WebJob in F#. Even if your WebJobs are in C#, it may be worth looking into using the new project type for its benefits.

    I’ve prepared a small repo with examples of a few WebJob project types.