Axxes IT Consultancy

Watching files with FAKE 5

Introduction

A while ago my colleagues and I had the opportunity to go to NDC Oslo. It was easily the best conference I’ve ever been to. NDC was very well organized and had a remarkable attention for detail.

From a content perspective I was most amused by the functional track. Great speakers like Mark Seemann, Don Syme, Scott Wlaschin and others really inspire when giving a talk. And besides their performance on stage, NDC enables the opportunity to meet al these experts afterwards.

One of the talks I attended was Immutable application deployments with F# Make by Nikolai Andersen. He talked about FAKE, a DSL for build tasks using F#. To me it struck like a very appealing alternative for PowerShell. I’m a fan of command-line interface and automation with scripts. PowerShell is my goto but I found an interesting case where FAKE 5 provided an ever so elegant solution.

Axxes at NDC Oslo

Current set-up

In my current WebAPI 2 OWIN project I’m using embedded resource files (.resx) for translations and expose these as json through an ApiController. This subsequently means that each time I add a new entry I need to update the binary and recompile. My frontend application, a full-blown Angular SPA is calling my backend to fetch the translations.

Imagine I’m implementing a new feature, after some initial design I first tackle the frontend. I don’t mock the translations api to come clean up front. Because of … well you know “reasons”… So, I just run my backend using IISExpress from PowerShell window.

This works fine until I need a new translation for a label on a page. Latterly I add one using a resx editor, recompile the binary and restart IISExpress. Here is where FAKE comes in, it is very easy to automate these steps. The final script will have dotnet watch vibe but as hinted earlier my application is still part of the Old World.

Installing FAKE

With latest major release of FAKE, it can be downloaded a dotnet cli tool.

dotnet tool install fake-cli --tool-path .\.fake

Next, I create a new file called build.fsx and add the following:

#r "paket:
nuget Fake.Core.Target prerelease"
#load "./.fake/build.fsx/intellisense.fsx"

open Fake.Core


// Default target
Target.create "Install" (fun _ ->
  Trace.trace "FAKE installed deps"
)

// start build
Target.runOrDefault "Install"

If we run .fake\fake.exe run build.fsx, it will install the dependencies (listed in that weird block #r paket:) and run the default target Install due to the last line.

Implementation idea

The plan is straightforward: we watch the *.resx files, launch IIS and if anything changes we stop IIS, recompile and restart. To succeed we will need a couple of new FAKE modules.

Add the following dependencies:

#r "paket:
nuget FSharp.Data 3.0.0-beta4
nuget Fake.IO.FileSystem
nuget Fake.DotNet.MsBuild
nuget Fake.Core.Target prerelease"

Remove the lock file, build.fsx.lock (this is a paket lock file). And run the same command again to have these installed.

rm build.fsx.lock
.fake\fake.exe run build.fsx

Watching the files

FAKE has a lot of modules out of the box. The FAKE.IO namespace contains file watching helpers. Let’s create a new Targetand watch all resx files.

open Fake.IO
open Fake.IO.Globbing.Operators

Target.create "WatchResx" (fun _ ->
  
    use watcher = !! "Translations\\*.resx" |> ChangeWatcher.run (fun changes ->
        printfn "%A" changes
    )   

    System.Console.ReadLine() |> ignore // keep the target open

    watcher.Dispose() // if you need to cleanup the watcher.
)

If we now run .fake\fake.exe run -t WatchResx and change a file matching the globbing pattern we see:

The last restore is still up to date. Nothing left to do.
run WatchResx
Building project with version: LocalBuild
Shortened DependencyGraph for Target WatchResx:
<== WatchResx

The running order is:
Group - 1
  - WatchResx
Starting target 'WatchResx'
seq [{FullPath = "C:\MySource\MyProject\Translations\resource.en.resx";
      Name = "resource.en.resx";
      Status = Changed;}]

Pressing enter will exit the Target and complete the run.

Finished (Success) 'WatchResx' in 00:01:41.6057151

---------------------------------------------------------------------
Build Time Report
---------------------------------------------------------------------
Target      Duration
------      --------
WatchResx   00:01:41.5999982
Total:      00:01:41.6991373
Status:     Ok
---------------------------------------------------------------------
Performance:
 - Cli parsing: 214 milliseconds
 - Packages: 14 milliseconds
 - Script analyzing: 19 milliseconds
 - Script running: 1 minute, 41 seconds
 - Script cleanup: 0 milliseconds
 - Runtime: 1 minute, 42 seconds

Starting and stopping IISExpress

Did I mention that everything in .NETCore is available in FAKE 5? This means that I can solve a lot of problem as if I were just writing simple .NET code. So how does one start a program in .NET? Using System.Diagnostics.Process and some FAKE helpers in the Process module.

open Fake.IO
open Fake.IO.Globbing.Operators
open System.Diagnostics

Target.create "WatchResx" (fun _ ->

    let startIIS () =
        Process.getProc(fun config ->
            { config with
                FileName = @"C:\Program Files\IIS Express\iisexpress.exe"
                Arguments = sprintf "/path:\"%s\" /port:5000" __SOURCE_DIRECTORY__
            }
        )

    let stopIIS () = 
        Process.GetProcessesByName("iisexpress")
        |> Seq.iter (fun p ->
            p.Kill()
        )

    let iisProcess:Process = startIIS()
    iisProcess.Start() |> ignore
  
    use watcher = !! "Translations\\*.resx" |> ChangeWatcher.run (fun changes ->
        printfn "%A" changes
        stopIIS()
        iisProcess.Start() |> ignore
    )   

    System.Console.ReadLine() |> ignore // keep the target open

    watcher.Dispose() // if you need to cleanup the watcher.
)

Building the source code

After the IIS process is stopped, we should recompile the project that contains the embedded resources. To easily achieve this, we can leverage the Fake.DotNet.MsBuild package.

open Fake.IO
open Fake.IO.Globbing.Operators
open System.Diagnostics
open Fake.DotNet

Target.create "WatchResx" (fun _ ->
    let csproj = sprintf "%s\\MyProject.csproj" __SOURCE_DIRECTORY__ 

    let buildProject () =
        MSBuild.build (fun buildParams ->
            { buildParams with 
                Verbosity = Some(Quiet)
                Targets = ["Build"]
                Properties =
                    [
                        "Optimize", "True"
                        "DebugSymbols", "False"
                        "Configuration", "Debug"
                    ]
            }
        ) csproj

    let startIIS () =
        Process.getProc(fun config ->
            { config with
                FileName = @"C:\Program Files\IIS Express\iisexpress.exe"
                Arguments = sprintf "/path:\"%s\" /port:5000" __SOURCE_DIRECTORY__
            }
        )

    let stopIIS () = 
        Process.GetProcessesByName("iisexpress")
        |> Seq.iter (fun p ->
            p.Kill()
        )
        
    buildProject()

    let iisProcess:Process = startIIS()
    iisProcess.Start() |> ignore
  
    use watcher = !! "Translations\\*.resx" |> ChangeWatcher.run (fun changes ->
        printfn "%A" changes
        stopIIS()
        buildProject()
        iisProcess.Start() |> ignore
    )   

    System.Console.ReadLine() |> ignore // keep the target open

    watcher.Dispose() // if you need to cleanup the watcher.
)

That first HTTP request

Once IISExpress has restarted, that first request will boot everything up and can be a bit slow. It would be nice if our Angular application isn’t the one that needs to wait the longest. What if we could trigger the initial request ourselves right after the restart. Using FSharp.Data we can trigger a request fire-and-forget style. Which brings us to the final code:

#r "paket:
nuget FSharp.Data 3.0.0-beta4
nuget Fake.IO.FileSystem
nuget Fake.DotNet.MsBuild
nuget Fake.Core.Target prerelease"
#load "./.fake/build.fsx/intellisense.fsx"

open Fake.Core

// Default target
Target.create "Install" (fun _ -> Trace.trace "FAKE installed deps")

open FSharp.Data
open Fake.DotNet
open Fake.IO
open Fake.IO.Globbing.Operators
open System.Diagnostics

Target.create "WatchResx" (fun _ ->
    let csproj = sprintf "%s\\MyProject.csproj" __SOURCE_DIRECTORY__

    let buildProject() =
        MSBuild.build (fun buildParams ->
            { buildParams with Verbosity = Some(Quiet)
                               Targets = [ "Build" ]
                               Properties =
                                   [ "Optimize", "True"
                                     "DebugSymbols", "False"
                                     "Configuration", "Debug" ] }) csproj

    let startIIS() =
        Process.getProc
            (fun config ->
            { config with FileName =
                              @"C:\Program Files\IIS Express\iisexpress.exe"
                          Arguments =
                              sprintf "/path:\"%s\" /port:5000"
                                  __SOURCE_DIRECTORY__ })

    let getTranslations() =
        // triggers web api
        Http.AsyncRequestString
            ("http://localhost:5000/api/translations?language=en")
        |> Async.Ignore
        |> Async.Start

    let stopIIS() =
        Process.GetProcessesByName("iisexpress") |> Seq.iter (fun p -> p.Kill())

    buildProject()

    let iisProcess : Process = startIIS()
    iisProcess.Start() |> ignore

    use watcher =
        !!"Translations\\*.resx"
        |> ChangeWatcher.run (fun changes ->
               printfn "%A" changes
               stopIIS()
               buildProject()
               iisProcess.Start() |> ignore
               getTranslations())

    System.Console.ReadLine() |> ignore // keep the target open


    watcher.Dispose() // if you need to cleanup the watcher.
)

// start build
Target.runOrDefault "Install"

Remarks

  • FAKE 5 is pretty well documented, if you are stuck you can always ask questions in the gitter channel.
  • Doing this in PowerShell gets ugly pretty quick.
  • F# is remarkable scripting language.

Final words

I hope you enjoyed this blogpost and it all makes sense.

Cheers, Florian

About the author

Florian Verdonck

Florian Verdonck

.NET Consultant

Share this article

GET TO KNOW US BETTER

Get to know Axxes and our corporate culture!

Looking for expertise or a new opportunity?

Let's get in touch!

Keep up with news and updates in the sector