You have 10 seconds to nixify your .Net project by Marcos Benevides

Figure 1: Don “The Gun” Syme, now officially part of the “Nix Gang”.

Figure 1: Don “The Gun” Syme, now officially part of the “Nix Gang”.

While this is a click bait title (maybe I’ve also farmed some zoomer credits out the OC), I’m pretty sure that’s how Don Syme feels when looks at a Result type like Result<'T,string> in an F# codebase. The reason why I’ve written this post is that I started doing changes in a open source project I develop/maintain, and realized other .Net people could benefit from this somehow (as most of the Nix + .Net guides are from before 2024).

An Opinionated Template

Turns out that, while writing this, I’ve decided to put everything in a template that’s easy to steal copy, so if you are from the future and have the Nix (the package manager) and the Flakes feature enabled you’ll be able to reproduce this post. Additionally, someone beat me into writing a flake for F#, here’s a more official take on it, there are a couple changes from my repo and this one, mostly because I tend to favor devenv (since it has shown better adoption when I preached it to non-Nixers).

Devenv

I’ve already tackled this in a previous article, but the best developer experience right now is with devenv.sh. Unlike the previous article tho, the environment setup is way simpler, just a bunch of .Net tools inside the shell and call it a day.

  #(...)
  # `nix develop --impure`
  default = devenv.lib.mkShell {
    inherit inputs pkgs;
    modules = [
      (
        { pkgs, lib, ... }:
        {
          packages = with pkgs; [
            bash
            just

            # for dotnet
            netcoredbg
            fsautocomplete
            fantomas
          ];

          languages.dotnet = {
            enable = true;
            package = dotnet;
          };

          # looks for the .env by default additionaly, there is .filename
          # if an arbitrary file is desired
          dotenv.enable = true;
        }
      )
    ];
  };
  # (...)

and similary to the previous post, we can access the environment by doing:

  nix develop --impure

or, if you have direnv, run direnv allow. Now you can build the project with the usual .Net tooling:

  dotnet build
  # There is also a justfile configured to you can just
  # type "just" and get all commands

Here’s a quick demo:

Build your project with Nix

This project is just a simple solution with the basic CLI Console App from the official documentation, the difference being that I’ve added a dependency to FsToolkit.ErrorHandling, just as a way to showcase how to handle nuget dependencies later. To package your .Net App there is already the buildDotNetModule, you can explore all the other options later in the docs, this is what works for the current code:

  # `nix build`
  default = pkgs.buildDotnetModule {
    pname = name;
    version = version;
    src = ./.;
    projectFile = "src/App/App.fsproj";
    nugetDeps = ./deps.nix;

    dotnet-sdk =
      with pkgs.dotnetCorePackages;
      combinePackages [
        sdk_8_0
      ];
    dotnet-runtime = pkgs.dotnetCorePackages.sdk_8_0;
  };

To properly build this with nix, you may have noticed that we also import a deps.nix file in the previous step. This file contains all the nuget dependencies our project uses and their hashes:

  { fetchNuGet }: [
    (fetchNuGet { pname = "FsToolkit.ErrorHandling"; version = "4.16.0"; hash = "sha256-4pRixOtRDgLt4/z71o1XnkuXRa/43LUhl/pDRpofX7s="; })
  ]

You don’t have to manually create or edit this, as its already documented here. In this project this is already handled by the just gen-deps (or just gd) command:

  $ rm deps.nix
  $ just gd
  dotnet restore --packages out
    Determining projects to restore...
    All projects are up-to-date for restore.
  nix run nixpkgs#nuget-to-nix -- out > deps.nix
  $ cat deps.nix
  { fetchNuGet }: [
    (fetchNuGet { pname = "FsToolkit.ErrorHandling"; version = "4.16.0"; hash = "sha256-4pRixOtRDgLt4/z71o1XnkuXRa/43LUhl/pDRpofX7s="; })
  ]

The previous step is also the usual way to update a .Net package in nixpkgs, most times you’ll just need to get the new version hash and update the nuget hashes as well:

  $ cd <my-clone-of-nixpkgs>
  $ nix-build -A <package-name>.passthru.fetch-deps | bash
  $ nix-build -A <package-name>

Then you open a PR to the official repository, following the contribution guidelines, of course. Now, going back to the testing the Nix build:

  $ nix build
  $ ./result/bin/App
  Test
  # It works

Generating OCI Images

Similar to the previous post, the Container Image looks like this:

  # nix build .#dockerImage
  dockerImage = pkgs.dockerTools.buildLayeredImage {
    name = "sample";
    tag = "latest";
    created = "now";
    contents = [ default ];
    config = {
      Cmd = [
        "${default}/bin/App"
      ];
    };
  };

And can be tested with:

  $ nix build .#dockerImage
  $ docker load < ./result
  $ docker container run --rm sample:latest
  Test

Conclusion

If this sparkled your interest somehow, here’s the source code, I’ve also made sure to configure some Github Actions Workflows there.

TODO