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][webjobs-vs-functions] 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][new-csproj] 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][target-frameworks] 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 `` in your web project’s .csproj with ``.
  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 ``
  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][vsts_task].

    ### 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][new-csproj].

    I’ve prepared a [small repo](https://github.com/jrr/webjob-project-examples) with examples of a few WebJob project types.

    [new-csproj]: https://github.com/dotnet/project-system/blob/master/docs/feature-comparison.md
    [compare-functions-and-webjobs]: https://docs.microsoft.com/en-us/azure/azure-functions/functions-compare-logic-apps-ms-flow-webjobs#compare-functions-and-webjobs
    [webjobs-vs-functions]: https://docs.microsoft.com/en-us/azure/azure-functions/functions-compare-logic-apps-ms-flow-webjobs#summary

    [vsts_task]: https://github.com/Microsoft/vsts-tasks/blob/master/Tasks/AzureRmWebAppDeploymentV4/README.md

    [singleton]: https://github.com/projectkudu/kudu/wiki/WebJobs-API#set-a-continuous-job-as-singleton
    [fake]: https://github.com/fsharp/FAKE
    [jobhost]: https://docs.microsoft.com/en-us/azure/app-service/webjobs-sdk-how-to#jobhost
    [dotnet]: https://www.microsoft.com/net/download/
    [target-frameworks]: https://docs.microsoft.com/en-us/dotnet/standard/frameworks
    [Zip]: https://fake.build/apidocs/v5/fake-io-zip.html
    [Kudu]: https://fake.build/apidocs/v5/fake-azure-kudu.html
    [webjobs sdk]: https://github.com/Azure/azure-webjobs-sdk
    [azure app service]: https://docs.microsoft.com/en-us/azure/app-service/
    [example-repo]: https://github.com/jrr/webjob-project-examples
    [azure portal]: https://azure.microsoft.com/en-us/features/azure-portal/