| name | dotnet-csproj-reading |
| description | Reads and modifies SDK-style .csproj files. PropertyGroup, ItemGroup, CPM, props. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
dotnet-csproj-reading
Teaches agents to read and safely modify SDK-style .csproj files. Covers project structure, PropertyGroup conventions, ItemGroup patterns, conditional expressions, Directory.Build.props/.targets, and central package management (Directory.Packages.props). Each subsection provides annotated XML examples and common modification patterns.
Scope
- SDK-style .csproj structure and SDK attribute conventions
- PropertyGroup and ItemGroup reading and modification
- Conditional expressions and TFM-based conditions
- Directory.Build.props/.targets and Central Package Management
Out of scope
- Project organization and SDK selection -- see [skill:dotnet-project-structure]
- Build error interpretation -- see [skill:dotnet-build-analysis]
- Common agent coding mistakes -- see [skill:dotnet-agent-gotchas]
Prerequisites
.NET 8.0+ SDK. SDK-style projects only (legacy .csproj format is not covered). MSBuild (included with .NET SDK).
Cross-references: [skill:dotnet-project-structure] for project organization and SDK selection, [skill:dotnet-build-analysis] for interpreting build errors from project misconfiguration, [skill:dotnet-agent-gotchas] for common project structure mistakes agents make.
Subsection 1: SDK-Style Project Structure
SDK-style projects use a <Project Sdk="..."> declaration that imports hundreds of default targets and props. Understanding what the SDK provides implicitly is essential to avoid redundant or conflicting declarations.
Annotated XML Example
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
</Project>
Common Modification Patterns
Changing SDK type -- when an agent creates a web project with the wrong SDK:
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
Disabling default globs -- rare, but needed when migrating from legacy format or when explicit file control is required:
<PropertyGroup>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<EnableDefaultItems>false</EnableDefaultItems>
</PropertyGroup>
Verifying which SDK a project uses:
head -1 src/MyApp/MyApp.csproj
Subsection 2: PropertyGroup Conventions
PropertyGroup elements contain scalar MSBuild properties. The most important properties control the target framework, language features, and output type.
Annotated XML Example
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Exe</OutputType>
<RootNamespace>MyApp.Api</RootNamespace>
<AssemblyName>MyApp.Api</AssemblyName>
<LangVersion>preview</LangVersion>
</PropertyGroup>
Common Modification Patterns
Enabling nullable for an existing project:
<Nullable>enable</Nullable>
Setting output type for a console app:
<OutputType>Exe</OutputType>
Adding TreatWarningsAsErrors (recommended for CI parity):
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
Subsection 3: ItemGroup Patterns
ItemGroup elements contain collections: package references, project references, file inclusions, and other build items. Understanding the three main item types prevents common agent mistakes.
Annotated XML Example
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115"
PrivateAssets="All" IncludeAssets="runtime;build;native;analyzers" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../MyApp.Core/MyApp.Core.csproj" />
<ProjectReference Include="../MyApp.Internal/MyApp.Internal.csproj"
PrivateAssets="All" />
</>
Common Modification Patterns
Adding a NuGet package:
dotnet add src/MyApp/MyApp.csproj package Microsoft.EntityFrameworkCore --version 9.0.0
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
Adding a project reference:
dotnet add src/MyApp.Api/MyApp.Api.csproj reference src/MyApp.Core/MyApp.Core.csproj
<ProjectReference Include="../MyApp.Core/MyApp.Core.csproj" />
Including non-compiled files in output:
<None Update="config/*.json" CopyToOutputDirectory="PreserveNewest" />
Subsection 4: Condition Expressions and Multi-Targeting
MSBuild conditions enable TFM-specific properties, platform-specific package references, and build configuration logic. Understanding condition syntax prevents broken multi-targeting builds.
Annotated XML Example
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Backport.System.Threading.Lock" Version="2.0.5" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
true
Common Modification Patterns
Adding a TFM:
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
Using version-agnostic TFM patterns for platform detection:
<ItemGroup Condition="$(TargetFramework.Contains('-android'))">
<AndroidResource Include="Resources/**" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0-android'">
Condition syntax reference:
| Expression | Meaning |
|---|
'$(Prop)' == 'value' | Exact match (case-insensitive) |
'$(Prop)' != 'value' | Not equal |
$(Prop.StartsWith('prefix')) | String starts with |
$(Prop.Contains('sub')) | String contains |
'$(Prop)' == '' | Property is empty/not set |
Exists('path') | File or directory exists |
Subsection 5: Directory.Build.props and Directory.Build.targets
These files centralize shared build configuration. MSBuild automatically imports Directory.Build.props (before the project) and Directory.Build.targets (after the project) from the current directory and all parent directories up to the filesystem root.
Annotated XML: Directory.Build.props
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup>
<Authors>MyCompany</Authors>
<Company>MyCompany</Company>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</>
Annotated XML: Directory.Build.targets
<Project>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0"
PrivateAssets="All" IncludeAssets="analyzers" />
</ItemGroup>
<ItemGroup Condition="'$(IsTestProject)' == 'true'">
<PackageReference Include="coverlet.collector" Version="8.0.0"
PrivateAssets="All" />
</ItemGroup>
<Target Name="PrintBuildInfo" AfterTargets="Build">
<Message Importance="high" Text="Built $(AssemblyName) for $(TargetFramework)" />
</Target>
</>
Common Modification Patterns
Hierarchy and override behavior:
repo-root/
Directory.Build.props <-- applies to ALL projects
src/
Directory.Build.props <-- applies to src/ projects only
MyApp.Api/
MyApp.Api.csproj <-- inherits from src/ props (NOT repo-root/)
MSBuild imports the nearest Directory.Build.props found walking upward. If a nested Directory.Build.props exists, it shadows the parent. To chain both, the nested file must explicitly import the parent:
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<RootNamespace>MyApp.$(MSBuildProjectName)</RootNamespace>
</PropertyGroup>
</Project>
When to use .props vs .targets:
| Use .props for | Use .targets for |
|---|
| Property defaults (TFM, nullable, etc.) | Items that depend on project properties |
| Package metadata (authors, license) | Custom build targets (AfterTargets, BeforeTargets) |
| Properties projects can override | Analyzer packages added to all projects |
Subsection 6: Directory.Packages.props (Central Package Management)
Central Package Management (CPM) centralizes NuGet package versions in a single Directory.Packages.props file. Individual projects reference packages without specifying versions.
Annotated XML Example
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
Project file with CPM enabled:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
</ItemGroup>
</Project>
Common Modification Patterns
Enabling CPM in an existing solution:
- Create
Directory.Packages.props at the solution root with <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>.
- Move all
Version attributes from PackageReference items into PackageVersion entries in the central file.
- Remove
Version from all PackageReference items in individual .csproj files.
grep -rn 'PackageReference Include=.*Version=' --include="*.csproj" src/
Overriding a version in a specific project (escape hatch):
<PackageReference Include="Microsoft.EntityFrameworkCore" VersionOverride="8.0.11" />
Hierarchical resolution: Directory.Packages.props resolves upward from the project directory, the same as Directory.Build.props. In monorepos, place the central file at the repo root. Sub-directories can have their own Directory.Packages.props -- the nearest one wins.
Migrating from per-project versions:
dotnet list src/MyApp.sln package --format json
Slopwatch Anti-Patterns
These patterns in project files indicate an agent is hiding problems rather than fixing them. See [skill:dotnet-slopwatch] for the automated quality gate that detects these patterns.
NoWarn in .csproj
<PropertyGroup>
<NoWarn>CS8600;CS8602;CS8604;IL2026;IL2046;IL3050</NoWarn>
</PropertyGroup>
<NoWarn> in the project file suppresses warnings for the entire project, making issues invisible. This is worse than #pragma because it has no scope boundary and cannot be audited per-file.
Fix: Remove <NoWarn> entries and fix the underlying issues. For warnings that genuinely do not apply project-wide, configure severity in .editorconfig instead:
[*.cs]
dotnet_diagnostic.CA2007.severity = none
Suppressed Analyzers in Directory.Build.props
<PropertyGroup>
<NoWarn>$(NoWarn);CA1062;CA1822;CA2007</NoWarn>
<RunAnalyzers>false</RunAnalyzers>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
</PropertyGroup>
Disabling analyzers in Directory.Build.props silences them across every project in the solution, including new projects added later. Agents sometimes do this to achieve a clean build quickly.
Fix: Keep analyzers enabled globally. Address warnings per-project or per-file. If a specific rule category does not apply (e.g., CA2007 in ASP.NET Core apps), suppress it in .editorconfig at the appropriate scope with a comment explaining why.
Cross-References
- [skill:dotnet-project-structure] -- SDK selection, project organization, solution layout
- [skill:dotnet-build-analysis] -- interpreting build errors caused by project misconfiguration
- [skill:dotnet-agent-gotchas] -- common project structure mistakes agents make (wrong SDK, broken refs)
References