Repository-Wide Configuration

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

A .NET repository is configured mostly by files at its root, not by the project files. Each is found automatically by walking up the directory tree from the project (or the current directory), so every project, every developer and every CI job gets the same settings without anyone passing a switch. This page lists those files, the tool that reads each one and when, and shows Ledgerline's complete set as run with .NET SDK 10.0.402. The introduction is in Namespaces, Assemblies and Projects — Sharing Configuration; .editorconfig rules themselves are described in Coding Conventions and Documentation.

File Read by Purpose Detail page

global.json

the dotnet host; the NuGet SDK resolver; dotnet test

SDK version and roll-forward, MSBuild project SDK versions, the test runner

Installing and Selecting SDKs

nuget.config

NuGet (restore, push, dotnet package)

package sources, source mapping, credentials, trusted signers, folders

nuget.config

Directory.Build.props

Microsoft.Common.props (early)

default properties for every project below it

Customizing the Build

Directory.Build.targets

Microsoft.Common.targets (late)

targets and late properties for every project below it

same

Directory.Build.rsp

the MSBuild command line (CLI builds only)

default command-line switches

Command-Line Reference

Directory.Packages.props

the SDK (imported by NuGet.props)

central package versions

Central Package Management

Directory.Solution.props/.targets

solution builds on the command line

solution-level hooks

Solutions and .slnx

.editorconfig, .globalconfig

the compiler and analyzers; IDEs

code style and analyzer severities

Format and Code Analysis

dotnet-tools.json or .config/dotnet-tools.json

dotnet tool restore, dotnet tool run

the local tool manifest

dotnet Tools

Where each file enters the build

flowchart TB RSP["Directory.Build.rsp: command-line switches"] --> START["MSBuild startup"] GJ["global.json: SDK and msbuild-sdks"] --> START START --> SDKPROPS["Sdk.props, then Microsoft.Common.props"] SDKPROPS --> DBP["Directory.Build.props (nearest one above the project)"] DBP --> NGP["obj/*.nuget.g.props (package props)"] NGP --> DPP["Directory.Packages.props (via NuGet.props)"] DPP --> PROJ["The .csproj body"] PROJ --> SDKT["Sdk.targets, then Microsoft.Common.targets"] SDKT --> NGT["obj/*.nuget.g.targets (package targets)"] NGT --> DBT["Directory.Build.targets (nearest one above the project)"] DBT --> REST["rest of the SDK targets"] NC["nuget.config: sources and mapping"] -.-> NGP EC[".editorconfig / .globalconfig"] -.-> CSC["CoreCompile (analyzers)"] REST --> CSC

The order in the middle column is what dotnet msbuild -pp showed for Ledgerline.Report on How a .NET Build Works: Directory.Build.props is the third file read, before the project body; Directory.Build.targets comes after the project body and after the package .targets, so it can override both.

The Ledgerline root files

ledgerline/
├── global.json
├── nuget.config
├── Directory.Build.props
├── Directory.Build.targets
├── Directory.Packages.props
├── dotnet-tools.json
├── .editorconfig
└── Ledgerline.slnx

global.json

{
  "sdk": {
    "version": "10.0.402",
    "rollForward": "latestPatch",
    "allowPrerelease": false
  },
  "test": {
    "runner": "Microsoft.Testing.Platform"
  }
}

sdk pins the SDK feature band (any 10.0.4xx patch at or above 402); test.runner switches dotnet test to the Microsoft.Testing.Platform mode (.NET 10). A third section, msbuild-sdks, would pin NuGet-delivered project SDKs.

nuget.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
    <add key="ledgerline-local" value="./artifacts/feed" />
    <add key="ledgerline-bagetter" value="http://localhost:5555/v3/index.json" allowInsecureConnections="True" />
  </packageSources>
  <packageSourceMapping>
    <packageSource key="nuget.org">
      <package pattern="*" />
    </packageSource>
    <packageSource key="ledgerline-local">
      <package pattern="Ledgerline" />
      <package pattern="Ledgerline.*" />
    </packageSource>
  </packageSourceMapping>
</configuration>

<clear /> drops every source inherited from user- and machine-level files, so the repository restores the same way everywhere. Package source mapping sends Ledgerline* to the local folder feed and everything else to nuget.org; the BaGetter source on localhost is used only for pushing in the publishing examples, so it has no mapping (and it needs allowInsecureConnections because it is plain HTTP). Credentials never go in this file — see Authenticated Feeds.

Directory.Build.props

<Project>
  <PropertyGroup>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <Authors>Ledgerline contributors</Authors>
    <Company>Ledgerline</Company>
    <Copyright>Copyright (c) Ledgerline contributors</Copyright>
    <RepositoryUrl>https://example.com/ledgerline/ledgerline</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <PackageProjectUrl>https://example.com/ledgerline</PackageProjectUrl>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageReadmeFile>README.md</PackageReadmeFile>
    <PackageIcon>icon.png</PackageIcon>
    <IsPackable>false</IsPackable>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
  </PropertyGroup>
  <PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true' or '$(TF_BUILD)' == 'true'">
    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
  </PropertyGroup>
</Project>

Defaults only: every value can be overridden in a project, which is how the four packable projects set IsPackable back to true. The CI condition reads environment variables, which evaluation turns into properties in its first pass (Reproducible and Deterministic Builds).

Directory.Build.targets

<Project>
  <ItemGroup Condition="'$(IsPackable)' == 'true'">
    <None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="\" Visible="false" />
    <None Include="$(MSBuildThisFileDirectory)icon.png" Pack="true" PackagePath="\" Visible="false" />
  </ItemGroup>
</Project>

This file must be a .targets file: it tests $(IsPackable), whose final value is known only after the project body has been read. In Directory.Build.props the condition would see the repository default (false) and never add the files. $(MSBuildThisFileDirectory) is the folder of the file that contains it, so the paths work from any project.

Directory.Packages.props

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Ledgerline" Version="1.0.0" />
    <PackageVersion Include="Ledgerline.Build" Version="1.0.0" />
    <PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
    <PackageVersion Include="System.Text.Json" Version="10.0.13" />
    <PackageVersion Include="Microsoft.Build.Utilities.Core" Version="18.9.6" />
    <PackageVersion Include="Microsoft.Build.Framework" Version="18.9.6" />
    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
    <PackageVersion Include="xunit.v3" Version="4.0.2" />
    <PackageVersion Include="xunit.runner.visualstudio" Version="4.0.1" />
  </ItemGroup>
</Project>

Projects then write <PackageReference Include="System.Text.Json" /> without a version.

.editorconfig and dotnet-tools.json

root = true

[*]
indent_style = space
insert_final_newline = true
charset = utf-8

[*.{csproj,props,targets,xml,slnx}]
indent_size = 2

[*.cs]
indent_size = 4
csharp_style_namespace_declarations = file_scoped:warning
dotnet_diagnostic.CA1822.severity = suggestion
{
  "version": 1,
  "isRoot": true,
  "tools": {
    "microsoft.dotnet.apicompat.tool": {
      "version": "10.0.402",
      "commands": [
        "apicompat"
      ],
      "rollForward": false
    }
  }
}

The tool manifest was not written by hand: since .NET 10, dotnet tool install <id> without -g creates a manifest when none exists. It walks up to the folder that contains .git and writes dotnet-tools.json there (the documentation also mentions a .config folder; both locations are searched):

dotnet tool install Microsoft.DotNet.ApiCompat.Tool --version 10.0.402
You can invoke the tool from this directory using the following commands: 'dotnet tool run apicompat' or 'dotnet apicompat'.
Tool 'microsoft.dotnet.apicompat.tool' (version '10.0.402') was successfully installed. Entry is added to the manifest file ~/ledgerline/dotnet-tools.json.

Directory.Build.props and .targets in detail

Props or targets?

The rule from the documentation: set properties that projects or the SDK read in Directory.Build.props, so that the project file and the SDK see them; put targets, and properties that depend on project values ($(IsPackable), $(TargetFramework), $(OutputType), anything a package sets) in Directory.Build.targets. The two classic mistakes are mirror images:

  • A TFM condition in Directory.Build.props: Condition="'$(TargetFramework)' == 'net8.0'" is false in every single-target project and in outer builds, because the project body has not set the property yet; it only works in the inner builds of multi-targeted projects, where TargetFramework is a global property.

  • A default in Directory.Build.targets: <Nullable>enable</Nullable> in the targets file overrides what the project set, instead of providing a default it can change.

Search scope and multi-level merging

MSBuild looks for Directory.Build.props in the project’s folder and then in every parent, and stops at the first one it finds. A tests/Directory.Build.props therefore replaces the root file for the test projects — unless it imports the parent explicitly with the GetPathOfFileAbove property function:

<Project>
  <!-- Import the repository-wide file first: MSBuild stops at the first Directory.Build.props it finds. -->
  <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

  <PropertyGroup>
    <IsPackable>false</IsPackable>
    <GenerateDocumentationFile>false</GenerateDocumentationFile>
    <!-- Tests may use obsolete members of the library on purpose. -->
    <NoWarn>$(NoWarn);CS0618</NoWarn>
  </PropertyGroup>
</Project>

Querying the test project shows the merged values — and, with the Import line removed, how much is silently lost:

cd tests/Ledgerline.Tests
dotnet msbuild -getProperty:Nullable -getProperty:TreatWarningsAsErrors -getProperty:NoWarn -getProperty:DirectoryBuildPropsPath
{
  "Properties": {
    "Nullable": "enable",
    "TreatWarningsAsErrors": "true",
    "NoWarn": ";CS0618",
    "DirectoryBuildPropsPath": "~/ledgerline/tests/Directory.Build.props"
  }
}
--- without the import:
{
  "Properties": {
    "Nullable": "",
    "TreatWarningsAsErrors": "false",
    "DirectoryBuildPropsPath": "~/ledgerline/tests/Directory.Build.props"
  }
}

The same applies to Directory.Build.targets (with GetPathOfFileAbove('Directory.Build.targets', …)). DirectoryBuildPropsPath shows which file was found; ImportDirectoryBuildProps=false (set before the import, for example in a -p: switch) turns the lookup off. On Linux and macOS the file name is case-sensitive: a directory.build.props is simply ignored.

Directory.Build.rsp

A response file holds command-line switches, one or more per line, # for comments. MSBuild reads Directory.Build.rsp from the project’s (or solution’s) folder upwards for command-line builds only — dotnet build, dotnet msbuild, msbuild.exe, not Visual Studio:

# Defaults for every command-line build under this folder.
-nodeReuse:false
-p:LedgerlineBuildOrigin=cli
cd src/Ledgerline
dotnet msbuild -getProperty:LedgerlineBuildOrigin
echo "[$(dotnet msbuild -noAutoResponse -getProperty:LedgerlineBuildOrigin)]"
cli
[]

-noAutoResponse (-noautorsp) ignores it. Because the IDE never reads it, Directory.Build.rsp should only hold switches that change how the build runs (-nodeReuse:false on CI, -warnAsMessage, logger settings), never properties that change what is built.

.editorconfig and .globalconfig

Both feed options and diagnostic severities to the compiler and analyzers (and, for code style, to the IDE). .editorconfig files apply to the files under their folder, by glob section; a global AnalyzerConfig file (.globalconfig, is_global = true) applies to every file of the project, whatever its path. When the same key is set in both, the documented precedence is: a later entry in the same file wins; between two .editorconfig files, the deeper one wins; between two global files, the higher global_level wins (.globalconfig defaults to 100); and between an .editorconfig and a global file, the .editorconfig wins. Compiler options (-nowarn, -warnaserror) beat all of them.

A three-step demonstration with a block-scoped namespace and EnforceCodeStyleInBuild set to true (code-style rules do not run at build time otherwise):

# .editorconfig
root = true

[*.cs]
csharp_style_namespace_declarations = file_scoped:warning
~/style/Accounts.cs(1,11): warning IDE0161: Convert to file-scoped namespace (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0161) [~/style/Style.csproj]

Adding a .globalconfig with dotnet_diagnostic.IDE0161.severity = error raises it to an error (the dotnet_diagnostic key beats the :warning suffix of the option):

~/style/Accounts.cs(1,11): error IDE0161: Convert to file-scoped namespace (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0161) [~/style/Style.csproj]

Adding dotnet_diagnostic.IDE0161.severity = none to the .editorconfig switches it off again, because the .editorconfig entry wins over the global file:

Build succeeded.

Templates for the root files

dotnet new creates each file with sensible defaults. The item templates on SDK 10.0.402:

dotnet new list --type item
Template Name                          Short Name                    Language    Tags
-------------------------------------  ----------------------------  ----------  --------------------------
dotnet gitattributes file              gitattributes,.gitattributes              Config
dotnet gitignore file                  gitignore,.gitignore                      Config
Dotnet local tool manifest file        tool-manifest                             Config
EditorConfig file                      editorconfig,.editorconfig                Config
global.json file                       globaljson,global.json                    Config
MSBuild Directory.Build.props file     buildprops                                MSBuild/props
MSBuild Directory.Build.targets file   buildtargets                              MSBuild/props
MSBuild Directory.Packages.props file  packagesprops                             MSBuild/packages/props/CPM
NuGet Config                           nugetconfig,nuget.config                  Config
…

(Only the configuration templates are shown.) Useful options: dotnet new globaljson --sdk-version 10.0.402 --roll-forward latestFeature, dotnet new buildprops --use-artifacts (adds ArtifactsPath for the artifacts output layout), dotnet new editorconfig --empty. The packagesprops template turns CPM on:

dotnet new packagesprops
cat Directory.Packages.props
<Project>
  <PropertyGroup>
    <!-- Enable central package management, https://learn.microsoft.com/en-us/nuget/consume-packages/Central-Package-Management -->
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
  </ItemGroup>
</Project>

Common mistakes

  • A nested Directory.Build.props without the GetPathOfFileAbove import — the root settings vanish for that subtree, often unnoticed until a warning that should fail the build does not.

  • Wrong file-name casing on Linux/macOS — the file is ignored.

  • Project-dependent conditions in .props, unconditional defaults in .targets — see above.

  • Putting build-changing properties in Directory.Build.rsp — Visual Studio builds differ from CLI builds.

  • A nuget.config without <clear /> — machine-level feeds leak into the repository’s restores.

  • Expecting .globalconfig to beat .editorconfig — it is the other way round.

Release notes

  • .NET 5: global AnalyzerConfig files; .NET 6: global_level precedence.

  • .NET 8: the artifacts output layout (dotnet new buildprops --use-artifacts).

  • .NET 10: test.runner in global.json; dotnet tool install creates a manifest automatically.