# 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
```bash
# Create a solution
dotnet new sln -n MySolution --format slnx
# Create projects
dotnet new classlib -n MyApp.Core
dotnet new webapi -n MyApp.Api
dotnet new mstest -n MyApp.Core.Tests
# Add projects to solution
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
# Add project references
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.
```xml
net9.0
latest
enable
enable
true
latest
true
true
true
true
true
true
0.1.0
https://github.com/your-org/your-repo
git
MIT
Your Team
true
snupkg
true
true
false
false
```
## Step 3: Enable Central Package Management
Create `Directory.Packages.props` at the repo root.
```xml
true
true
```
## Step 4: Add Packages and References via CLI
```bash
# Add packages (version comes from Directory.Packages.props)
dotnet add MyApp.Core/MyApp.Core.csproj package Microsoft.Extensions.Logging.Abstractions
dotnet add MyApp.Api/MyApp.Api.csproj package Microsoft.Extensions.Hosting
# Remove packages
dotnet remove MyApp.Core/MyApp.Core.csproj package Microsoft.Extensions.Logging.Abstractions
# List packages and references
dotnet list MyApp.Core/MyApp.Core.csproj package
dotnet list MyApp.Api/MyApp.Api.csproj reference
```
## Step 5: Pin SDK Version with global.json
```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`.
```xml
```
```xml
```
## 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
```bash
# Create test project
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
# Add test packages
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
# Run tests with coverage
dotnet test MyApp.Core.Tests/MyApp.Core.Tests.csproj -- --coverage
```
## Validation Commands
```bash
dotnet restore # Verify packages resolve
dotnet build # Verify compilation
dotnet test # Verify tests pass
dotnet pack # Verify NuGet packages
```
## Best Practices
1. **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.
2. **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.
3. **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.
4. **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.
5. **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.
6. **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.
7. **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.
8. **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.
9. **Add `false` for test projects using MSBuild conditions** to prevent accidental NuGet package creation from test assemblies during `dotnet pack` operations.
10. **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.