Using F# and FAKE to build a SharePoint provider-hosted app

28 Apr 2014

This post outlines a real-world (anonymized) example of how to use FAKE to create a build script that gets the latest source code from Team Foundation Server (TFS), builds the SharePoint 2013 provider-hosted ASP.NET MVC/API app, and then takes it for a quick spin.

In terms of Visual Studio solution structure, the provider-hosted app is created using the build-in project template. It results in the creation of two projects: one that holds the declarative logic (mostly XML, no .NET code) of the app to be deployed to SharePoint and one that's a regular ASP.NET MVC/WebAPI app to be hosted outside SharePoint. Deploying the declarative app to SharePoint configures SharePoint to seamlessly integrate with the MVC/WebAPI app.

In order to build the solution consisting of the two projects, we need to go through the following steps:

  1. Clean up from any previous build by deleting a TFS workspace and associated source control folder
  2. Create a new source control folder and associate it with a new TFS workspace
  3. Get the latest source from TFS and put it into the new source control folder
  4. Build the MVC/WebAPI and the SharePoint app projects and package the latter for deployment to SharePoint
  5. Adjust the MVC/WebAPI app's web.config to use LocalDB instead of MSSQL Server. The LocalDB database is stored inside app's App_Data folder which makes resetting the environment easier than with MSSQL Server
  6. Host the MVC/WebAPI app under IISExpress and request /home/createDatabase. This'll trigger code-first Entity Framework database creation and verifies that the seed code is working

We could've included deployment to SharePoint as an additional step, but focusing on the MVC/WebAPI app is currently what adds the most value.

Step 0: Establish build context

Before getting to the actual build steps, called targets in FAKE, we must first load relevant FAKE assemblies, added to the solution through Nuget, and define various settings required by the targets. Defining these settings at the top of the script makes each target relatively independant of actual values and avoids having too many magic string and numbers around.

#r "packages/FAKE.2.15.4/tools/FakeLib.dll"
#r "packages/FAKE.2.15.4/tools/Fake.IIS.dll"

open System.Net
open Fake 
open Fake.IISExpress

// common settings
let baseDir = "c:/users/ronnie/desktop/AcmeApp-CI"
let relativeSourceDir = "AcmeRoot/DevBranch/AcmeApp"
let absoluteSourceDir = baseDir @@ relativeSourceDir
let workspaceName = "AcmeApp-CI"
let projectCollection = "AcmeCorp.visualstudio.com\DefaultCollection"
let mvcAppPort = 8080

// external tools
let tf = "C:/Program Files (x86)/Microsoft Visual Studio 12.0/Common7/IDE/tf.exe"

Step 1: Delete TFS workspace and source control folder

To minimize the risk of any previous build affecting the current one, producing false positives or false negatives, we disconnect the source control folder from its associated TFS workspace and delete both. This step might fail on first build or if a previous build failed to complete the target. In either case, we don't want the entire build to fail and thus probe and react to the tf.exe exit code.

Target "DeleteWorkspace" <| fun _ ->
    let args = sprintf "workspace /delete /noprompt %s" workspaceName
    let exitCode = Shell.Exec(tf, args)

    // exitCode 100 means "The workspace does not exist"
    if not (exitCode = 0 || exitCode = 100) then 
        failwithf "Unable to delete workspace: %i" exitCode
    FileHelper.DeleteDir(baseDir)

Step 2: Create source control folder and TFS workspace

Next we (re)create the source control folder and associate it with a TFS workspace. For tf.exe to properly setup this relationship, it's important we set the current directory of tf.exe to the recently created source control folder. As a side-effect tf.exe creates a special $tf folder for storing metadata within it's current directory.

Target "Createworkspace" <| fun _ ->
    FileHelper.CreateDir(baseDir)  
    let args = sprintf "workspace /new /noprompt %s /collection:%s" workspaceName projectCollection
    let exitCode = Shell.Exec(tf, args, baseDir)
    if exitCode <> 0 then failwithf "Unable to create workspace: %i" exitCode

Step 3: Get latest source code

From inside the TFS project collection, we want to recursively retrieve the part of the directory structure that maps to our source code. This can potentially be a long path that includes both solution, project, and branch folders:

Target "GetLatest" <| fun _ ->
    let args = sprintf "get \"$/%s\" /recursive" relativeSourceDir
    let exitCode = Shell.Exec(tf, args, baseDir)
    if exitCode <> 0 then failwithf "Unable to get latest: %i" exitCode

Step 4: Build apps

Compiling the SharePoint and the MVC/WebAPI apps is likely the easiest part of the entire build process. It's simply a matter of delegating to MSBuild. One thing to note though is that in addition to the typical MSBuild build target, SharePoint app projects have a special "Package" target that created the package to deploy to SharePoint. To create this package the SharePoint app project must first be compiled, which has a dependency on the MVC/WebAPI app, compiling it as well.

Target "BuildApp" <| fun _ ->
    // because of build dependencies this'll build both SharePoint and MVC/WebAPI projects
    MSBuildDebug "" "Package" [absoluteSourceDir @@ "AcmeAppMVC/AcmeApp.csproj"]
    |> ignore

Step 5 and 6: Deploy provided-hosted app

As a quick test, we want to make sure the web server can actually run our MVC/WebAPI app. We could deploy the app to IIS but it works just as well with IISExpress. All we need to do is point IISExpress to the MVC/WebAPI folder and tell it which port to use. Before starting IISExpress, however, we modify web.config to point to a LocalDB instead of a MSSQL Server database.

Target "Deploy" <| fun _ -> 
    // iisexpress requires the passed in path to be normalizes to distinguish its arguments
    let mvc = normalizeFileName (absoluteSourceDir @@ "AcmeAppMVC")

    // update web.config connection string used by Entity Framework
    updateConnectionString
        "AcmeAppDatabase"
        @"Server=(localdb)\v11.0;AttachDBFilename=|DataDirectory|\AcmeApp.mdf;Integrated Security=true;"
        (mvc @@ "web.config")      

    // iisexpres is automatically killed by FAKE when the script terminates
    ProcessHelper.StartProcess(fun psi -> 
        psi.FileName <- IISExpressDefaults.ToolPath
        psi.Arguments <- sprintf "\"/path:%s\" /port:%i" mvc mvcAppPort)

    // database creation with EF requires data directory to exists beforehand
    FileHelper.CreateDir(mvc @@ "App_Data")    

    // iisexpress starts asynchronously which on rare occasions seems to lead to 
    // the server not being fully operational when it receives the request, 
    // resulting in an Internal Server Error exception.
    let triggerDatabaseCreation = sprintf "http://localhost:%i/home/CreateDatabase" mvcAppPort
    (new WebClient()).DownloadString(triggerDatabaseCreation) |> ignore

Wrapping up

Lastly, we connect the targets together in a dependency chain. Every target ultimately depends on DeleteWorkspace to execute first and no target depends on Deploy. In other words, if we ask FAKE to execute the Deploy target, it'll trigger the execution of all the other targets starting with DeleteWorkspace.

Target "Default" DoNothing

"DeleteWorkspace" ==> "CreateWorkspace" ==> "GetLatest" ==> "BuildApp" ==> "Deploy"
RunTargetOrDefault "Default"