1# Dotnet {#dotnet}
2
3## Local Development Workflow {#local-development-workflow}
4
5For local development, it's recommended to use nix-shell to create a dotnet environment:
6
7```nix
8# shell.nix
9with import <nixpkgs> { };
10
11mkShell {
12 name = "dotnet-env";
13 packages = [ dotnet-sdk ];
14}
15```
16
17### Using many sdks in a workflow {#using-many-sdks-in-a-workflow}
18
19It's very likely that more than one sdk will be needed on a given project. Dotnet provides several different frameworks (E.g dotnetcore, aspnetcore, etc.) as well as many versions for a given framework. Normally, dotnet is able to fetch a framework and install it relative to the executable. However, this would mean writing to the nix store in nixpkgs, which is read-only. To support the many-sdk use case, one can compose an environment using `dotnetCorePackages.combinePackages`:
20
21```nix
22with import <nixpkgs> { };
23
24mkShell {
25 name = "dotnet-env";
26 packages = [
27 (
28 with dotnetCorePackages;
29 combinePackages [
30 sdk_8_0
31 sdk_9_0
32 ]
33 )
34 ];
35}
36```
37
38This will produce a dotnet installation that has the dotnet 8.0 9.0 sdk. The first sdk listed will have its cli utility present in the resulting environment. Example info output:
39
40```ShellSession
41$ dotnet --info
42.NET SDK:
43 Version: 9.0.100
44 Commit: 59db016f11
45 Workload version: 9.0.100-manifests.3068a692
46 MSBuild version: 17.12.7+5b8665660
47
48Runtime Environment:
49 OS Name: nixos
50 OS Version: 25.05
51 OS Platform: Linux
52 RID: linux-x64
53 Base Path: /nix/store/a03c70i7x6rjdr6vikczsp5ck3v6rixh-dotnet-sdk-9.0.100/share/dotnet/sdk/9.0.100/
54
55.NET workloads installed:
56There are no installed workloads to display.
57Configured to use loose manifests when installing new manifests.
58
59Host:
60 Version: 9.0.0
61 Architecture: x64
62 Commit: 9d5a6a9aa4
63
64.NET SDKs installed:
65 8.0.404 [/nix/store/6wlrjiy10wg766490dcmp6x64zb1vc8j-dotnet-core-combined/share/dotnet/sdk]
66 9.0.100 [/nix/store/6wlrjiy10wg766490dcmp6x64zb1vc8j-dotnet-core-combined/share/dotnet/sdk]
67
68.NET runtimes installed:
69 Microsoft.AspNetCore.App 8.0.11 [/nix/store/6wlrjiy10wg766490dcmp6x64zb1vc8j-dotnet-core-combined/share/dotnet/shared/Microsoft.AspNetCore.App]
70 Microsoft.AspNetCore.App 9.0.0 [/nix/store/6wlrjiy10wg766490dcmp6x64zb1vc8j-dotnet-core-combined/share/dotnet/shared/Microsoft.AspNetCore.App]
71 Microsoft.NETCore.App 8.0.11 [/nix/store/6wlrjiy10wg766490dcmp6x64zb1vc8j-dotnet-core-combined/share/dotnet/shared/Microsoft.NETCore.App]
72 Microsoft.NETCore.App 9.0.0 [/nix/store/6wlrjiy10wg766490dcmp6x64zb1vc8j-dotnet-core-combined/share/dotnet/shared/Microsoft.NETCore.App]
73
74Other architectures found:
75 None
76
77Environment variables:
78 Not set
79
80global.json file:
81 Not found
82
83Learn more:
84 https://aka.ms/dotnet/info
85
86Download .NET:
87 https://aka.ms/dotnet/download
88```
89
90## dotnet-sdk vs dotnetCorePackages.sdk {#dotnet-sdk-vs-dotnetcorepackages.sdk}
91
92The `dotnetCorePackages.sdk_X_Y` is preferred over the old dotnet-sdk as both major and minor version are very important for a dotnet environment. If a given minor version isn't present (or was changed), then this will likely break your ability to build a project.
93
94## dotnetCorePackages.sdk vs dotnetCorePackages.runtime vs dotnetCorePackages.aspnetcore {#dotnetcorepackages.sdk-vs-dotnetcorepackages.runtime-vs-dotnetcorepackages.aspnetcore}
95
96The `dotnetCorePackages.sdk` contains both a runtime and the full sdk of a given version. The `runtime` and `aspnetcore` packages are meant to serve as minimal runtimes to deploy alongside already built applications.
97
98## Packaging a Dotnet Application {#packaging-a-dotnet-application}
99
100To package Dotnet applications, you can use `buildDotnetModule`. This has similar arguments to `stdenv.mkDerivation`, with the following additions:
101
102* `projectFile` is used for specifying the dotnet project file, relative to the source root. These have `.sln` (entire solution) or `.csproj` (single project) file extensions. This can be a list of multiple projects as well. When omitted, will attempt to find and build the solution (`.sln`). If running into problems, make sure to set it to a file (or a list of files) with the `.csproj` extension - building applications as entire solutions is not fully supported by the .NET CLI.
103* `nugetDeps` should be a path to a JSON file, a path to a nix file (deprecated), a derivation, or a list of derivations. A `deps.json` file can be generated using the script attached to `passthru.fetch-deps`, which is the preferred method. All `nugetDeps` packages are added to `buildInputs`.
104::: {.note}
105For more detail about managing the `deps.json` file, see [Generating and updating NuGet dependencies](#generating-and-updating-nuget-dependencies)
106:::
107
108* `packNupkg` is used to pack project as a `nupkg`, and installs it to `$out/share`. If set to `true`, the derivation can be used as a dependency for another dotnet project by adding it to `buildInputs`.
109* `buildInputs` can be used to resolve `ProjectReference` project items. Referenced projects can be packed with `buildDotnetModule` by setting the `packNupkg = true` attribute and passing a list of derivations to `buildInputs`. Since we are sharing referenced projects as NuGets they must be added to csproj/fsproj files as `PackageReference` as well.
110 For example, your project has a local dependency:
111 ```xml
112 <ProjectReference Include="../foo/bar.fsproj" />
113 ```
114 To enable discovery through `buildInputs` you would need to add:
115 ```xml
116 <ProjectReference Include="../foo/bar.fsproj" />
117 <PackageReference Include="bar" Version="*" Condition=" '$(ContinuousIntegrationBuild)'=='true' "/>
118 ```
119* `executables` is used to specify which executables get wrapped to `$out/bin`, relative to `$out/lib/$pname`. If this is unset, all executables generated will get installed. If you do not want to install any, set this to `[]`. This gets done in the `preFixup` phase.
120* `runtimeDeps` is used to wrap libraries into `LD_LIBRARY_PATH`. This is how dotnet usually handles runtime dependencies.
121* `buildType` is used to change the type of build. Possible values are `Release`, `Debug`, etc. By default, this is set to `Release`.
122* `selfContainedBuild` allows to enable the [self-contained](https://docs.microsoft.com/en-us/dotnet/core/deploying/#publish-self-contained) build flag. By default, it is set to false and generated applications have a dependency on the selected dotnet runtime. If enabled, the dotnet runtime is bundled into the executable and the built app has no dependency on .NET.
123* `useAppHost` will enable creation of a binary executable that runs the .NET application using the specified root. More info in [Microsoft docs](https://learn.microsoft.com/en-us/dotnet/core/deploying/#publish-framework-dependent). Enabled by default.
124* `useDotnetFromEnv` will change the binary wrapper so that it uses the .NET from the environment. The runtime specified by `dotnet-runtime` is given as a fallback in case no .NET is installed in the user's environment. This is most useful for .NET global tools and LSP servers, which often extend the .NET CLI and their runtime should match the user's .NET runtime.
125* `dotnet-sdk` is useful in cases where you need to change what dotnet SDK is being used. You can also set this to the result of `dotnetSdkPackages.combinePackages`, if the project uses multiple SDKs to build.
126* `dotnet-runtime` is useful in cases where you need to change what dotnet runtime is being used. This can be either a regular dotnet runtime, or an aspnetcore.
127* `testProjectFile` is useful in cases where the regular project file does not contain the unit tests. It gets restored and build, but not installed. You may need to regenerate your nuget lockfile after setting this. Note that if set, only tests from this project are executed.
128* `testFilters` is used to disable running unit tests based on various [filters](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test#filter-option-details). This gets passed as: `dotnet test --filter "{}"`, with each filter being concatenated using `"&"`.
129* `disabledTests` is used to disable running specific unit tests. This gets passed as: `dotnet test --filter "FullyQualifiedName!={}"`, to ensure compatibility with all unit test frameworks.
130* `dotnetRestoreFlags` can be used to pass flags to `dotnet restore`.
131* `dotnetBuildFlags` can be used to pass flags to `dotnet build`.
132* `dotnetTestFlags` can be used to pass flags to `dotnet test`. Used only if `doCheck` is set to `true`.
133* `dotnetInstallFlags` can be used to pass flags to `dotnet install`.
134* `dotnetPackFlags` can be used to pass flags to `dotnet pack`. Used only if `packNupkg` is set to `true`.
135* `dotnetFlags` can be used to pass flags to all of the above phases.
136
137When packaging a new application, you need to fetch its dependencies. Create an empty `deps.json`, set `nugetDeps = ./deps.json`, then run `nix-build -A package.fetch-deps` to generate a script that will build the lockfile for you.
138
139Here is an example `default.nix`, using some of the previously discussed arguments:
140```nix
141{
142 lib,
143 buildDotnetModule,
144 dotnetCorePackages,
145 ffmpeg,
146}:
147
148let
149 referencedProject = import ../../bar {
150 # ...
151 };
152in
153buildDotnetModule rec {
154 pname = "someDotnetApplication";
155 version = "0.1";
156
157 src = ./.;
158
159 projectFile = "src/project.sln";
160 nugetDeps = ./deps.json; # see "Generating and updating NuGet dependencies" section for details
161
162 buildInputs = [
163 referencedProject
164 ]; # `referencedProject` must contain `nupkg` in the folder structure.
165
166 dotnet-sdk = dotnetCorePackages.sdk_8_0;
167 dotnet-runtime = dotnetCorePackages.runtime_8_0;
168
169 executables = [ "foo" ]; # This wraps "$out/lib/$pname/foo" to `$out/bin/foo`.
170 executables = [ ]; # Don't install any executables.
171
172 packNupkg = true; # This packs the project as "foo-0.1.nupkg" at `$out/share`.
173
174 runtimeDeps = [ ffmpeg ]; # This will wrap ffmpeg's library path into `LD_LIBRARY_PATH`.
175}
176```
177
178Keep in mind that you can tag the [`@NixOS/dotnet`](https://github.com/orgs/nixos/teams/dotnet) team for help and code review.
179
180## Dotnet global tools {#dotnet-global-tools}
181
182[.NET Global tools](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools) are a mechanism provided by the dotnet CLI to install .NET binaries from Nuget packages.
183
184They can be installed either as a global tool for the entire system, or as a local tool specific to project.
185
186The local installation is the easiest and works on NixOS in the same way as on other Linux distributions.
187[See dotnet documentation](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools#install-a-local-tool) to learn more.
188
189[The global installation method](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools#install-a-global-tool)
190should also work most of the time. You have to remember to update the `PATH`
191value to the location the tools are installed to (the CLI will inform you about it during installation) and also set
192the `DOTNET_ROOT` value, so that the tool can find the .NET SDK package.
193You can find the path to the SDK by running `nix eval --raw nixpkgs#dotnet-sdk` (substitute the `dotnet-sdk` package for
194another if a different SDK version is needed).
195
196This method is not recommended on NixOS, since it's not declarative and involves installing binaries not made for NixOS,
197which will not always work.
198
199The third, and preferred way, is packaging the tool into a Nix derivation.
200
201### Packaging Dotnet global tools {#packaging-dotnet-global-tools}
202
203Dotnet global tools are standard .NET binaries, just made available through a special
204NuGet package. Therefore, they can be built and packaged like every .NET application,
205using `buildDotnetModule`.
206
207If however the source is not available or difficult to build, the
208`buildDotnetGlobalTool` helper can be used, which will package the tool
209straight from its NuGet package.
210
211This helper has the same arguments as `buildDotnetModule`, with a few differences:
212
213* `pname` and `version` are required, and will be used to find the NuGet package of the tool
214* `nugetName` can be used to override the NuGet package name that will be downloaded, if it's different from `pname`
215* `nugetHash` is the hash of the fetched NuGet package. `nugetSha256` is also supported, but not recommended. Set this to `lib.fakeHash` for the first build, and it will error out, giving you the proper hash. Also remember to update it during version updates (it will not error out if you just change the version while having a fetched package in `/nix/store`)
216* `dotnet-runtime` is set to `dotnet-sdk` by default. When changing this, remember that .NET tools fetched from NuGet require an SDK.
217
218Here is an example of packaging `pbm`, an unfree binary without source available:
219```nix
220{ buildDotnetGlobalTool, lib }:
221
222buildDotnetGlobalTool {
223 pname = "pbm";
224 version = "1.3.1";
225
226 nugetHash = "sha256-ZG2HFyKYhVNVYd2kRlkbAjZJq88OADe3yjxmLuxXDUo=";
227
228 meta = {
229 homepage = "https://cmd.petabridge.com/index.html";
230 changelog = "https://cmd.petabridge.com/articles/RELEASE_NOTES.html";
231 license = lib.licenses.unfree;
232 platforms = lib.platforms.linux;
233 };
234}
235```
236## Generating and updating NuGet dependencies {#generating-and-updating-nuget-dependencies}
237
238When writing a new expression, you can use the generated `fetch-deps` script to initialise the lockfile.
239After setting `nugetDeps` to the desired location of the lockfile (e.g. `./deps.json`),
240build the script with `nix-build -A package.fetch-deps` and then run the result.
241(When the root attr is your package, it's simply `nix-build -A fetch-deps`.)
242
243There is also a manual method:
244First, restore the packages to the `out` directory, ensure you have cloned
245the upstream repository and you are inside it.
246
247```bash
248$ dotnet restore --packages out
249 Determining projects to restore...
250 Restored /home/ggg/git-credential-manager/src/shared/Git-Credential-Manager/Git-Credential-Manager.csproj (in 1.21 sec).
251```
252
253Next, use the `nuget-to-json` tool provided in Nixpkgs to generate a lockfile to `deps.json` from
254the packages inside the `out` directory.
255
256```bash
257$ nuget-to-json out > deps.json
258```
259The `nuget-to-json` tool will generate an output similar to the one below
260```json
261[
262 {
263 "pname": "Avalonia",
264 "version": "11.1.3",
265 "hash": "sha256-kz+k/vkuWoL0XBvRT8SadMOmmRCFk9W/J4k/IM6oYX0="
266 },
267 {
268 "pname": "Avalonia.Angle.Windows.Natives",
269 "version": "2.1.22045.20230930",
270 "hash": "sha256-RxPcWUT3b/+R3Tu5E5ftpr5ppCLZrhm+OTsi0SwW3pc="
271 },
272 {
273 "pname": "Avalonia.BuildServices",
274 "version": "0.0.29",
275 "hash": "sha256-WPHRMNowRnYSCh88DWNBCltWsLPyOfzXGzBqLYE7tRY="
276 },
277 // ...
278 {
279 "pname": "System.Runtime.CompilerServices.Unsafe",
280 "version": "6.0.0",
281 "hash": "sha256-bEG1PnDp7uKYz/OgLOWs3RWwQSVYm+AnPwVmAmcgp2I="
282 },
283 {
284 "pname": "System.Security.Cryptography.ProtectedData",
285 "version": "4.5.0",
286 "hash": "sha256-Z+X1Z2lErLL7Ynt2jFszku6/IgrngO3V1bSfZTBiFIc="
287 },
288 {
289 "pname": "Tmds.DBus.Protocol",
290 "version": "0.16.0",
291 "hash": "sha256-vKYEaa1EszR7alHj48R8G3uYArhI+zh2ZgiBv955E98="
292 }
293]
294
295```
296
297Finally, you move the `deps.json` file to the appropriate location to be used by `nugetDeps`, then you're all set!
298
299If you ever need to update the dependencies of a package, you instead do
300
301* `nix-build -A package.fetch-deps` to generate the update script for `package`
302* Run `./result` to regenerate the lockfile to the path passed for `nugetDeps` (keep in mind if it can't be resolved to a local path, the script will write to `$1` or a temporary path instead)
303* Finally, ensure the correct file was written and the derivation can be built.