Embedding F# Compiler: NuGet References

UPDATE: If you are looking for a ready to use solution please check out my fsc-host NuGet package. Also I wrote a blog post about it.

In the previous post I showed how to extend an F# application with scripting capabilities. This time I want to extend the previous example with the #r directive support and resolving NuGet dependencies.

On high level we need to extract NuGet packages information from the script and pass it to the compiler as additional arguments. Before compiling we also need to dynamically load the referenced dlls to the host application. I first took the naive approach of parsing and extracting the directives from AST and feeding them into FSharpDependencyManager. It worked to an extent. Luckily, FsChecker comes with a built-in solution. We can use GetProjectOptionsFromScript and subsequently ParseAndCheckProject methods. That way we can retrieve DependencyFiles and extract resolved dlls' paths.

let resolveNugets () =
  async {
    let source = File.ReadAllText script.path |> SourceText.ofString
    let! projOptions, errors = checker.GetProjectOptionsFromScript(script.path, source)

    match errors with
    | [] -> 
      let! projResults = checker.ParseAndCheckProject(projOptions)
      return
        match projResults.HasCriticalErrors with
        | false -> 
          projResults.DependencyFiles 
            |> Seq.choose(
              function
              | path when path.EndsWith(".dll") -> Some path
              | _ -> None)
            |> Seq.groupBy id
            |> Seq.map (fun (path, _) -> path)
            |> Ok
        | _ -> Error (ScriptParseError (projResults.Errors |> Seq.map string) )
    | _ -> return Error (ScriptParseError (errors |> Seq.map string) )
  }

Fig. 1. Get resolved dlls' paths for all referenced NuGet packages


Now we can enrich the compiler arguments and load the assemblies.

let refs = nugetResolutions |> Seq.map(fun path -> $"-r:{path}")
nugetResolutions |> Seq.iter (fun r -> Assembly.LoadFrom r |> ignore)

let compilerArgs = [|
  "-a"; script.path
  "--targetprofile:netcore"
  "--target:module"
  yield! refs
  sprintf "-r:%s" (Assembly.GetEntryAssembly().GetName().Name)
  "--langversion:preview"
|]

Fig. 2. Load resolved dlls to the host application and add their paths to the compiler arguments

This post was supposed to be longer as I tried to go the hard way. Fortunately I found an easier and more reliable method. I hope this and the previous post will help you with hosting the F# compiler in your own apps. I’ll keep experimenting with it. The full source code for this post is available here.