| name | msbuild-csproj |
| description | USE FOR: Structuring MSBuild .csproj files, Directory.Build.props, Directory.Packages.props, Central Package Management, and dotnet CLI workflows for modern .NET project scaffolding. DO NOT USE FOR: Hand-editing .csproj XML directly (use dotnet CLI), runtime application configuration (use appsettings.json), or build scripting (use Cake or NUKE).
|
| license | MIT |
| metadata | {"displayName":"MSBuild CSPROJ","author":"Tyler-R-Kendrick","version":"1.0.0"} |
| compatibility | ["claude","copilot","cursor"] |
| references | [{"title":"MSBuild Reference","url":"https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-reference"},{"title":".NET Project SDK Overview","url":"https://learn.microsoft.com/en-us/dotnet/core/project-sdk/overview"},{"title":"Central Package Management Documentation","url":"https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management"}] |
MSBuild CSPROJ Structuring for Modern .NET
Overview
This skill provides explicit, step-by-step guidance for structuring MSBuild .csproj files for modern .NET using Directory.Build.props, Central Package Management (CPM), and the dotnet CLI. All scaffolding and all modifications to .csproj files MUST be performed with the dotnet CLI. The goal is to keep individual .csproj files minimal while centralizing build properties, package versions, and shared configuration at the repository root.
Modern .NET SDK-style projects use a declarative XML format that is dramatically simpler than the legacy verbose format. Combined with Directory.Build.props for shared properties and Directory.Packages.props for centralized version management, a well-structured repository eliminates redundancy and version drift across projects.
Prerequisites
- .NET 9+ SDK installed
dotnet CLI available in PATH
- Basic understanding of MSBuild and NuGet
Rules You Must Follow
- Use
dotnet CLI for scaffolding projects and solutions.
- Use
dotnet CLI for ALL .csproj changes (packages, references, frameworks, properties).
- Do NOT hand-edit
.csproj files.
- It is OK to create or edit
Directory.Build.props, Directory.Packages.props, .editorconfig, and global.json directly.
- Prefer the .NET Aspire framework for multi-project or distributed apps.
- Project names must be unique across the solution to avoid
slnx conflicts.
Step 1: Create a Solution and Projects
dotnet new sln -n MySolution --format slnx
dotnet new classlib -n MyApp.Core
dotnet new webapi -n MyApp.Api
dotnet new mstest -n MyApp.Core.Tests
dotnet sln MySolution.slnx add MyApp.Core/MyApp.Core.csproj
dotnet sln MySolution.slnx add MyApp.Api/MyApp.Api.csproj
dotnet sln MySolution.slnx add MyApp.Core.Tests/MyApp.Core.Tests.csproj
dotnet add MyApp.Api/MyApp.Api.csproj reference MyApp.Core/MyApp.Core.csproj
dotnet add MyApp.Core.Tests/MyApp.Core.Tests.csproj reference MyApp.Core/MyApp.Core.csproj
Step 2: Create Directory.Build.props
Create Directory.Build.props at the repo root with shared properties for all projects.
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<VersionPrefix>0.1.0</VersionPrefix>
</PropertyGroup>
<PropertyGroup Condition="'$(IsPackable)' == 'true'">
<RepositoryUrl>https://github.com/your-org/your-repo</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Authors>Your Team</Authors>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('.Tests'))">
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
Step 3: Enable Central Package Management
Create Directory.Packages.props at the repo root.
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Resilience" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageVersion Include="MSTest.TestFramework" Version="3.4.0" />
<PackageVersion Include="MSTest.Sdk" Version="3.4.0" />
<PackageVersion Include="Microsoft.Testing.Platform" Version="1.4.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="1.4.0" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.133" />
</ItemGroup>
<ItemGroup>
<GlobalPackageReference Include="Nerdbank.GitVersioning" />
</ItemGroup>
</Project>
Step 4: Add Packages and References via CLI
dotnet add MyApp.Core/MyApp.Core.csproj package Microsoft.Extensions.Logging.Abstractions
dotnet add MyApp.Api/MyApp.Api.csproj package Microsoft.Extensions.Hosting
dotnet remove MyApp.Core/MyApp.Core.csproj package Microsoft.Extensions.Logging.Abstractions
dotnet list MyApp.Core/MyApp.Core.csproj package
dotnet list MyApp.Api/MyApp.Api.csproj reference
Step 5: Pin SDK Version with global.json
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestFeature"
}
}
Resulting .csproj (Minimal)
After following these steps, individual .csproj files are minimal because properties come from Directory.Build.props and versions from Directory.Packages.props.
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
<ProjectReference Include="..\MyApp.Core\MyApp.Core.csproj" />
</ItemGroup>
</Project>
Common MSBuild Properties Reference
| Property | Purpose | Typical Value |
|---|
TargetFramework | Target .NET version | net9.0 |
LangVersion | C# language version | latest |
Nullable | Nullable reference types | enable |
ImplicitUsings | Auto-import common namespaces | enable |
TreatWarningsAsErrors | Fail build on any warning | true |
Deterministic | Reproducible builds | true |
IsPackable | Whether project produces a NuGet package | true / false |
GenerateDocumentationFile | Produce XML doc file | true |
RestorePackagesWithLockFile | Generate packages.lock.json | true |
ManagePackageVersionsCentrally | Enable CPM | true |
CentralPackageTransitivePinningEnabled | Pin transitive dependency versions | true |
Testing with Microsoft Testing Platform
dotnet new mstest -n MyApp.Core.Tests
dotnet sln MySolution.slnx add MyApp.Core.Tests/MyApp.Core.Tests.csproj
dotnet add MyApp.Core.Tests/MyApp.Core.Tests.csproj reference MyApp.Core/MyApp.Core.csproj
dotnet add MyApp.Core.Tests/MyApp.Core.Tests.csproj package MSTest.TestFramework
dotnet add MyApp.Core.Tests/MyApp.Core.Tests.csproj package Microsoft.Testing.Platform
dotnet add MyApp.Core.Tests/MyApp.Core.Tests.csproj package Microsoft.Testing.Extensions.CodeCoverage
dotnet test MyApp.Core.Tests/MyApp.Core.Tests.csproj -- --coverage
Validation Commands
dotnet restore
dotnet build
dotnet test
dotnet pack
Best Practices
-
Never hand-edit .csproj files -- use dotnet add package, dotnet add reference, and dotnet remove commands exclusively to prevent malformed XML, merge conflicts, and divergence from CPM version definitions.
-
Centralize all shared build properties in Directory.Build.props at the repository root and use MSBuild conditions (Condition="$(MSBuildProjectName.EndsWith('.Tests'))") for per-project overrides instead of duplicating properties across .csproj files.
-
Enable Central Package Management by setting ManagePackageVersionsCentrally to true in Directory.Packages.props and define every package version there; individual .csproj files should reference packages without version attributes.
-
Enable CentralPackageTransitivePinningEnabled to pin transitive dependency versions so that all projects in the solution resolve the same version of shared transitive packages, preventing diamond dependency conflicts.
-
Set RestorePackagesWithLockFile to true and commit packages.lock.json files to ensure deterministic restores; CI builds should use dotnet restore --locked-mode to fail on any version drift.
-
Use ContinuousIntegrationBuild conditionally with Condition="'$(CI)' == 'true'" so that deterministic build metadata (source link, path mapping) is only applied in CI where it is needed, not during local development.
-
Keep individual .csproj files to fewer than 20 lines by moving all PropertyGroup settings to Directory.Build.props; a well-configured project file should contain only PackageReference and ProjectReference items.
-
Use global.json with rollForward: latestFeature to pin the SDK major.minor version while allowing patch updates, ensuring all developers and CI agents use a compatible SDK without requiring exact version matches.
-
Add <IsPackable>false</IsPackable> for test projects using MSBuild conditions to prevent accidental NuGet package creation from test assemblies during dotnet pack operations.
-
Run dotnet restore, dotnet build, and dotnet test as separate validation steps rather than relying on implicit restore in dotnet build, so that restore failures are reported clearly and cached restore results are used efficiently.