This repo aims to be a reference of setting up multiple .NET F# projects.
Due to the nature of these things, it is a snapshot, created in 04/2025, where the lates .NET LTS version was 8.0. Versions and tooling can and will change in the future.
That said, this repo is organized to show the timeline of setup steps via commits. Commits are grouped via their description to tasks, with the same description as the sections in this document:
Table of contents:
This guide assumes that the user has a version of the .NET 8 SDK installed.
No assumptions regarding choice of IDE (e.g., VSCode, Visual Studio, or Rider) are made.
Where possible, pure .NET CLI commands are used.
Note that most commands can also be done via the IDEs.
All CLI commands are run from the project root folder if not specified otherwise.
This section describes how to create a new F# library project and publish it to NuGet.
Libraries are collections of reusable code that can be consumed by other projects.
Solution files are used to organize multiple projects within a single repository.
Use the .NET CLI command:
dotnet new sln -n MySolutiondotnet new slncreates a new solution file.-nspecifies the name of the solution.
Create an F# library project within the solution folder.
Usually, a src folder is created that will contain all projects of the solution.
dotnet new classlib -lang F# -n MyLibrary -o ./src/MyLibrary -f netstandard2.0dotnet new classlibcreates a new library project.-lang F#specifies the language to be used.-nspecifies the name of the project.-ospecifies the output directory for the project.-fspecifies the target framework for the project. Libraries are usually built againstnetstandardto be compatible with multiple .NET runtimes.
Then, add it to the solution:
dotnet sln ./MySolution.sln add ./src/MyLibrary/MyLibrary.fsprojdotnet sln addadds the project to a solution../MySolutionis the path to the solution file../src/MyLibrary/MyLibrary.fsprojis the path to the project file generated in the previous step.
Building a project or solution compiles the source code, resolving dependencies and generating the necessary output, such as executables for applications or binaries for libraries.
You can build single projects:
dotnet build ./src/MyLibrary/MyLibrary.fsprojor the entire solution:
dotnet build ./MySolution.slnyou can also just run dotnet build in the solution folder, and it will find the solution file.
The classlib template used in a previous step already created a MyLibrary.fs file.
Time to add some code.
In F#, code is organized using namespaces and modules to structure functionality and avoid naming conflicts.
A namespace groups related modules and types, while modules encapsulate related functions and values:
namespace MyLibrary
module MathFunctions =
let add x y = x + y
let subtract x y = x - y
module StringUtils =
let toUpper (s: string) = s.ToUpper()
let toLower (s: string) = s.ToLower()There are no CLI commands to add a new source file to a project, it has to be done manually.
First, create a new file in the project folder, e.g., NewLibrary.fs, containing an empty namespace:
namespace NewLibraryThen, add the following code to the MyLibrary.fsproj:
<Compile Include="NewLibrary.fs" />This tells the project to include the new file in the compilation process.
Most IDEs have a Add file functionality that will do this for you.
You can use modules defined in other files in the same project. Keep in mind that an F# project is compiled in order`, so the order of the files in the project matters:
<Compile Include="Library.fs" />
<Compile Include="NewLibrary.fs" />Means you will be able to use code defined in Library.fs in NewLibrary.fs, but not the other way around.
To use the modules defined in MyLibrary.fs, you can use the open keyword to import them into your code:
namespace NewLibrary
module Results =
let result1 = MyLibrary.MathFunctions.add 1 2Nuget packages are a way to distribute reusable code libraries and tools in the .NET ecosystem.
Generate a NuGet package from the library project using:
dotnet pack ./src/MyLibrary/MyLibrary.fsproj -o ./nupkgdotnet packcreates a NuGet package from the project../src/MyLibrary/MyLibrary.fsprojis the path to the project file.-ospecifies the output directory for the package.
This creates a .nupkg file in the ./nupkg folder.
You can also pack the entire solution using:
dotnet pack ./MySolution.sln -o ./nupkgNuGet is a package registry for .NET.
After creating an account, you can upload .nupkg packages manually or via the CLI.
To publish the package using the CLI, you need to set up an API key on your account page.
Then, run the following command:
dotnet nuget push ./nupkg/MyLibrary.1.0.0.nupkg -k YOUR_API_KEY_HEREdotnet nuget pushuploads the package to NuGet../nupkg/MyLibrary.1.0.0.nupkgis the path to the package file.-kspecifies the API key for authentication.
This section describes how to create a new F# console application project and wrap it as a .NET tool. Console applications are standalone programs that can be executed from the command line.
Create an F# console application using:
dotnet new console -lang F# -n MyConsoleApp -o ./src/MyConsoleApp -f net8.0dotnet new consolecreates a new console application project.-lang F#specifies the language to be used.-nspecifies the name of the project.-ospecifies the output directory for the project.-fspecifies the target framework for the project. For applications, it is usually best to target the latest version of .NET., as you get performance improvements and new features.
Then, add it to the solution:
dotnet sln ./MySolution.sln add ./src/MyConsoleApp/MyConsoleApp.fsprojTo use the library in the console application, add a project reference:
dotnet add ./src/MyConsoleApp/MyConsoleApp.fsproj reference ./src/MyLibrary/MyLibrary.fsprojdotnet add referenceadds a project reference to the console application project../src/MyConsoleApp/MyConsoleApp.fsprojis the path to the console application project file../src/MyLibrary/MyLibrary.fsprojis the path to the library project file.
This will create this reference in the MyConsoleApp.fsproj:
<ProjectReference Include="..\MyLibrary\MyLibrary.fsproj">You could also add a reference to the library project via NuGet, but that would require publishing the library first:
dotnet add ./src/MyConsoleApp/MyConsoleApp.fsproj package MyLibrary --version 1.0.0dotnet add packageadds a NuGet package reference to the console application project.MyLibraryis the name of the package.
Applications must end with the main entry point, meaning the last file in the project must contain a function that is executed when the application is run.
The file created by the dotnet new console command is called Program.fs.
When this file simply ends with a function that returns unit, it implicitly becomes the main entry point:
printfn "Hello from F#"This will print Hello from F# to the console when the application is run.
You can also define the entry point explicitly using the main function. This has several advantages for more complex programs, such as allowing you to specify command-line arguments and return values.
[<EntryPoint>]
let main argv =
printfn $"""Hello from F#. You provided the following arguments: {argv |> String.concat ", "}""" // access command-line arguments from argv
0 // return an integer exit codeRun the application with:
dotnet run --project ./src/MyConsoleApp/MyConsoleApp.fsprojyou can also build the project and run the executable directly:
dotnet build --project ./src/MyConsoleApp/MyConsoleApp.fsproj
./src/MyConsoleApp/bin/Debug/net8.0/MyConsoleApp.exeTo pass command-line arguments to the application, use:
dotnet run --project ./src/MyConsoleApp/MyConsoleApp.fsproj -- arg1 arg2 arg3or when running the executable directly:
dotnet build --project ./src/MyConsoleApp/MyConsoleApp.fsproj
./src/MyConsoleApp/bin/Debug/net8.0/MyConsoleApp.exe arg1 arg2 arg3.NET tools are command-line applications that can be installed and executed locally or globally via the .NET CLI. For this you will need to add tool metadata to the project file:
<PackAsTool>true</PackAsTool>
<ToolCommandName>mytool</ToolCommandName>Then, Package the console application:
dotnet pack ./src/MyConsoleApp/MyConsoleApp.fsproj -o ./nupkgYou can test the tool locally by installing it from the package:
dotnet tool install -g --source .\nupkg MyConsoleApp
dotnet tool installinstalls a .NET tool.-gspecifies that the tool should be installed globally.--sourcespecifies the source of the package. In this case, it is the localnupkgfolder.MyConsoleAppis the name of the tool.
and then running it:
dotnet mytool arg1 arg2 arg3If this nuget package is published to NuGet, you will be able to install it via the CLI:
dotnet tool install --global MyConsoleApp --version 1.0.0Tests are essential for ensuring the correctness and reliability of your code. Unit tests are small, isolated tests that aim to verify the behavior of individual components or functions.
This section describes how to test the library and console application projects and perform them in a CI/CD pipeline.
there exist multiple testing frameworks for .NET, such as xUnit, or Expecto.
xUnit is a more general .NET testing framework which isa heavily used in C# projects, while Expecto is more focused on F# and functional programming.
This written guide will explicitly use xUnit, but the repository contains an equivalent example of using Expecto as well.
test projects are special console applications that are used to run tests.
Create an F# xUnit test project using:
dotnet new xunit -lang F# -n XUnitTests -o ./tests/XUnitTests -f net8.0Then, add it to the solution:
dotnet sln ./MySolution.sln add ./tests/XUnitTests/XUnitTests.fsprojReference the project under test:
dotnet add ./tests/XUnitTests/XUnitTests.fsproj reference ./src/MyLibrary/MyLibrary.fsprojπ Commit for this chapter (xUnit)
π Commit for this chapter (Expecto)
Unit tests in .NET are usually organized in classes that show the compiler that they are containing tests.
These classes then contain methods that each represent a single unit test.
Usually, these methods names are verbose to indicate what the test should verify.
In xUnit, you can use the Fact attribute to mark a method as a test case.
Inside these methods, you can use Assert methods to verify the actual behavior of your code against the expected behavior.
namespace XUnitTests
open Xunit
open MyLibrary
module MathFunctionsTests =
[<Fact>]
let ``Adding 2 and 2 returns 4`` () =
let result = MathFunctions.add 2 2
Assert.Equal(4, result)
[<Fact>]
let ``Subtracting 2 from 4 returns 2`` () =
let result = MathFunctions.subtract 4 2
Assert.Equal(2, result)Execute all tests in a solution using:
dotnet testor run tests in a specific project using:
dotnet test ./tests/XUnitTests/XUnitTests.fsprojπ Commit for this chapter (xUnit)
π Commit for this chapter (Expecto)
You can set up GitHub to run tests automatically when you push code to the repository or create a pull request.
A basic GitHub Actions workflow can be set up in .github/workflows/build.yml to run:
name: Build and Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # checkout repo
- name: Setup .NET
uses: actions/setup-dotnet@v4 # install dotnet
with:
dotnet-version: 8.x.x
- name: Build and test # run dotnet test
working-directory: ./
run: dotnet testThis workflow will run the tests every time you push code to the main branch or create a pull request against it.