简体   繁体   中英

Load dotnet project files in F# Interactive

I have a F# dotnet project with some dependencies in my .fsproj (eg <PackageReference Include="FSharp.Data" Version="3.0.0-beta4" /> ).

Now I want to test some functions in a module I wrote, so I start fsharpi and try to load my module with #load "Program.fs";; .

However, then I get error FS0039: The type 'CsvProvider' is not defined .

How can I load my module with the correct dependencies?

I have seen workarounds which include manually loading all required dll s from some obscure system dependent path (eg #load a package in F# interactive (FSharpChart.fsx) ), but I guess there must be a better way to do this.

In order to get access to the code in imported libraries, you need to tell the FSI to load them.

At the lowest level, this can be done with an #r directive:

#r "./packages/FSharp.Data/lib/whatever/FSharp.Data.dll"

This is often sufficient for one-off purposes.

However, if there are a lot of references, your IDE can usually automate this for you. For example, in Visual Studio, you can right-click the project and select either "Send References to F# Interactive" or "Generate Script File with References" (not sure about the exact wording). The same options are available in VSCode if you have the Ionide extension installed.

Also, there is an active proposal+prototype for supporting packages in FSI directly - see https://github.com/fsharp/fslang-suggestions/issues/542 . However, that is not yet merged and seems to have stalled a bit.

There are a few option for doing this, none of them perfect.

Ionide

If you are using Ionide, then you can right click on a project in F# view and choose "Generate References for FSI". This will create an F# script that loads all of the dependencies and project files for you. Write your test code below this.

The downsides of this approach:

  • You will need to run the procedure every time the project files or dependencies change.
  • Not applicable where you don't have access to VS Code

Paket

If you are using Paket, then you can generate "load scripts" for your project:

dotnet paket generate-load-scripts

This will generate F# scripts in ./paket/load . There will be a load script for each dependency (and its transitive dependencies) and for each Paket "group".

This can automatically sync with your paket.dependencies if you add this line:

generate_load_scripts: true

See: https://fsprojects.github.io/Paket/paket-generate-load-scripts.html

The downsides of this approach:

  • It may load more dependencies than you actually need, which has a performance cost
  • You must #load you project files manually

Dotnet + Nuget

New with .NET 5, you can write Nuget strings directly in your fsx files. This is a great feature for quick scripts!

#r "nuget: FParsec"

// Package with a specific version
// #r "nuget: FParsec,1.1.1"

open FParsec

#load "./Types.fs"

// etc...

The downsides of this approach:

  • It does not sync package versions with Paket, if you are using that for your project code
  • You must #load you project files manually

One Day?

In an ideal world, we would be able to do something like this:

#r "fsproj: ./my-proj.fsproj"

open FParsec

// etc...

There is an issue for this here: https://github.com/dotnet/fsharp/issues/8764

As requested in the comments on Fyodor's answer, here's a script that I've used in the past to generate the #r and #load directives required to load an .fsproj in F# Interactive:

module References

#r "System.Xml"
#r "System.Xml.Linq"

open System
open System.IO
open System.Linq
open System.Xml.Linq

let inline isNotNull o = o |> isNull |> not

let private getProject path =
    Directory.EnumerateFiles(path, "*.*proj") |> Seq.head |> XDocument.Load    

let generateDlls path =
    let projectFile = getProject path
    let references =
        projectFile.Descendants <| XName.Get("HintPath", "http://schemas.microsoft.com/developer/msbuild/2003")
        |> Seq.filter (fun reference -> reference.Value.ToLower().EndsWith(".dll"))
        |> Seq.filter (fun reference -> reference.Value.StartsWith("$") |> not)
        |> Seq.map (fun reference -> reference.Value)
    let projects =
        projectFile.Descendants <| XName.Get("ProjectReference", "http://schemas.microsoft.com/developer/msbuild/2003")
        |> Seq.map (fun reference -> reference.Elements(XName.Get("Name", "http://schemas.microsoft.com/developer/msbuild/2003")).SingleOrDefault())
        |> Seq.filter (fun nameElement -> nameElement |> isNotNull)
        |> Seq.map (fun nameElement -> nameElement.Value)
        |> Seq.map (fun reference -> sprintf "../%s/bin/debug/%s.dll" reference reference)

    references 
    |> Seq.append projects
    |> Seq.iter (fun reference -> printfn "#r @\"%s\"" reference)

let generateFs path =
    let projectFile = getProject path
    projectFile.Descendants <| XName.Get("Compile", "http://schemas.microsoft.com/developer/msbuild/2003")
    |> Seq.map (fun reference -> reference.Attribute("Include" |> XName.op_Implicit))
    |> Seq.filter (fun reference -> reference |> isNotNull && reference.Value.ToLower().EndsWith("fs"))
    |> Seq.filter (fun reference -> reference.Value.Equals("AssemblyInfo.fs", StringComparison.CurrentCultureIgnoreCase) |> not)
    |> Seq.iter (fun reference -> printfn "#load @\"%s\"" reference.Value)


// Example Usage:
// #load @"GenerateReferences.fsx"
// References.generateDlls __SOURCE_DIRECTORY__
// References.generateFs __SOURCE_DIRECTORY__

I'm not sure this is completely perfect, for instance, I don't think it gets dependency-ordering right. However, it should be a reasonable starting point if you want to enhance it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM