Repository-Wide Configuration
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its 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 |
|---|---|---|---|
|
the |
SDK version and roll-forward, MSBuild project SDK versions, the test runner |
|
|
NuGet (restore, push, |
package sources, source mapping, credentials, trusted signers, folders |
|
|
|
default properties for every project below it |
|
|
|
targets and late properties for every project below it |
same |
|
the MSBuild command line (CLI builds only) |
default command-line switches |
|
|
the SDK (imported by |
central package versions |
|
|
solution builds on the command line |
solution-level hooks |
|
|
the compiler and analyzers; IDEs |
code style and analyzer severities |
|
|
|
the local tool manifest |
Where each file enters the build
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.
Source: MSBuild: Import order.
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.
Source: .NET: global.json overview.
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.
Source: NuGet: Central Package Management.
.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, whereTargetFrameworkis 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.
Source: MSBuild: Directory.Build.rsp.
.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.
Source: .NET: EditorConfig, Global AnalyzerConfig, Precedence.
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.propswithout theGetPathOfFileAboveimport — 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.configwithout<clear />— machine-level feeds leak into the repository’s restores. -
Expecting
.globalconfigto beat.editorconfig— it is the other way round.