Skill

project-conventions

This skill should be used when creating or editing .csproj files, managing NuGet packages, configuring Directory.Build.props or Directory.Packages.props, organizing .NET solutions, or setting up global.json and .editorconfig.

From ccfg-csharp
Install
1
Run in your terminal
$
npx claudepluginhub jsamuelsen11/claude-config --plugin ccfg-csharp
Tool Access

This skill uses the workspace's default tool permissions.

Skill Content

.NET Project Conventions and Build Configuration

This skill defines comprehensive conventions for .NET project structure, Central Package Management, build configuration, solution organization, and tooling setup.

SDK-Style .csproj Structure

Minimal .csproj for Source Projects

Source project files should be as minimal as possible. Properties shared across all projects belong in Directory.Build.props, not in each .csproj.

<!-- CORRECT: Minimal .csproj relying on Directory.Build.props -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <RootNamespace>Catalog.Application</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\Catalog.Domain\Catalog.Domain.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="MediatR" />
  </ItemGroup>

</Project>
<!-- WRONG: Duplicating properties that belong in Directory.Build.props -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <LangVersion>latest</LangVersion>
    <RootNamespace>Catalog.Application</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\Catalog.Domain\Catalog.Domain.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="MediatR" Version="12.4.1" />
  </ItemGroup>

</Project>

Web API Project Uses Web SDK

<!-- CORRECT: Web SDK for ASP.NET Core projects -->
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <RootNamespace>Catalog.Api</RootNamespace>
  </PropertyGroup>

</Project>
<!-- WRONG: Standard SDK with manual ASP.NET references -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <RootNamespace>Catalog.Api</RootNamespace>
    <OutputType>Exe</OutputType>
  </PropertyGroup>

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
  </ItemGroup>

</Project>

Test Project Inherits from tests/Directory.Build.props

<!-- CORRECT: Test project with minimal configuration -->
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\..\src\Catalog.Application\Catalog.Application.csproj" />
  </ItemGroup>

</Project>
<!-- WRONG: Test project duplicating shared test dependencies -->
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\Catalog.Application\Catalog.Application.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
    <PackageReference Include="xunit" Version="2.9.2" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
    <PackageReference Include="FluentAssertions" Version="6.12.2" />
    <PackageReference Include="NSubstitute" Version="5.3.0" />
    <PackageReference Include="coverlet.collector" Version="6.0.2" />
  </ItemGroup>

</Project>

Central Package Management (CPM)

Always Use Directory.Packages.props

All package versions must be declared centrally. Individual .csproj files reference packages without version numbers.

<!-- CORRECT: Directory.Packages.props declares all versions -->
<Project>

  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
  </PropertyGroup>

  <ItemGroup>
    <PackageVersion Include="MediatR" Version="12.4.1" />
    <PackageVersion Include="FluentValidation" Version="11.11.0" />
    <PackageVersion Include="xunit" Version="2.9.2" />
  </ItemGroup>

</Project>
<!-- CORRECT: .csproj references without Version -->
<ItemGroup>
  <PackageReference Include="MediatR" />
  <PackageReference Include="FluentValidation" />
</ItemGroup>
<!-- WRONG: Version on PackageReference when CPM is enabled -->
<ItemGroup>
  <PackageReference Include="MediatR" Version="12.4.1" />
</ItemGroup>

Group Packages by Category in CPM

<!-- CORRECT: Grouped and commented -->
<Project>

  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <!-- ASP.NET Core -->
  <ItemGroup>
    <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
    <PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
  </ItemGroup>

  <!-- Entity Framework Core -->
  <ItemGroup>
    <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
    <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.11" />
  </ItemGroup>

  <!-- Testing -->
  <ItemGroup>
    <PackageVersion Include="xunit" Version="2.9.2" />
    <PackageVersion Include="FluentAssertions" Version="6.12.2" />
    <PackageVersion Include="NSubstitute" Version="5.3.0" />
  </ItemGroup>

</Project>
<!-- WRONG: Flat, unsorted, no grouping -->
<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="xunit" Version="2.9.2" />
    <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.9.0" />
    <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
    <PackageVersion Include="NSubstitute" Version="5.3.0" />
    <PackageVersion Include="Serilog.AspNetCore" Version="8.0.3" />
  </ItemGroup>
</Project>

Enable Transitive Pinning

Always enable CentralPackageTransitivePinningEnabled to control transitive dependency versions.

<!-- CORRECT: Transitive pinning enabled -->
<PropertyGroup>
  <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<!-- WRONG: Missing transitive pinning -->
<PropertyGroup>
  <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

Directory.Build.props

Root Props for Shared Settings

Place at the solution root. Applies to all projects in the directory tree.

<!-- CORRECT: Shared properties at solution root -->
<Project>

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <LangVersion>latest</LangVersion>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <Deterministic>true</Deterministic>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" PrivateAssets="all" />
  </ItemGroup>

</Project>
<!-- WRONG: Missing critical settings -->
<Project>

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

</Project>

Test Props Import Parent Then Add Test-Specific Settings

<!-- CORRECT: tests/Directory.Build.props -->
<Project>

  <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

  <PropertyGroup>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
    <NoWarn>$(NoWarn);CA1707</NoWarn>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" />
    <PackageReference Include="FluentAssertions" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" />
    <PackageReference Include="NSubstitute" />
    <PackageReference Include="xunit" />
    <PackageReference Include="xunit.runner.visualstudio" />
  </ItemGroup>

</Project>
<!-- WRONG: Not importing parent props -->
<Project>

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

  <!-- Lost all parent settings: Nullable, TreatWarningsAsErrors, analyzers -->

</Project>

Solution Layout

Layered Architecture with src/ and tests/

CORRECT:
project-root/
├── src/
│   ├── MyApp.Api/           (Web layer)
│   ├── MyApp.Application/   (Business logic)
│   ├── MyApp.Domain/        (Domain models)
│   └── MyApp.Infrastructure/ (Data access, external)
├── tests/
│   ├── Directory.Build.props (test-specific)
│   ├── MyApp.Application.Tests/
│   ├── MyApp.Domain.Tests/
│   └── MyApp.Api.Tests/
├── Directory.Build.props     (shared)
├── Directory.Packages.props  (CPM)
├── global.json
├── .editorconfig
└── MyApp.sln
WRONG:
project-root/
├── MyApp.Api/
├── MyApp.Application/
├── MyApp.Domain/
├── MyApp.Infrastructure/
├── MyApp.Tests/             (single test project for everything)
└── MyApp.sln

Test Project Mirrors Source Project

CORRECT:
src/Catalog.Application/Services/ProductService.cs
tests/Catalog.Application.Tests/Services/ProductServiceTests.cs

src/Catalog.Domain/Models/Order.cs
tests/Catalog.Domain.Tests/Models/OrderTests.cs
WRONG:
src/Catalog.Application/Services/ProductService.cs
tests/Tests/ProductServiceTest.cs

src/Catalog.Domain/Models/Order.cs
tests/Tests/OrderTests.cs

global.json

Always Pin SDK Version

{
  "sdk": {
    "version": "8.0.404",
    "rollForward": "latestPatch",
    "allowPrerelease": false
  }
}
// WRONG: No global.json at all
// Different developers may use different SDK versions,
// causing inconsistent builds

Use latestPatch Roll-Forward

{
  "sdk": {
    "version": "8.0.404",
    "rollForward": "latestPatch"
  }
}
// WRONG: latestMajor allows any SDK
{
  "sdk": {
    "version": "8.0.404",
    "rollForward": "latestMajor"
  }
}

.editorconfig

Must Exist at Solution Root

Every .NET solution must have an .editorconfig at the root. At minimum, it must set:

# CORRECT: Minimum .editorconfig
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

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

[*.cs]
csharp_style_namespace_declarations = file_scoped:warning
dotnet_style_qualification_for_field = false:warning

Enforce File-Scoped Namespaces

# CORRECT: Enforce file-scoped namespaces
[*.cs]
csharp_style_namespace_declarations = file_scoped:warning
# WRONG: No namespace style enforcement
[*.cs]
csharp_style_namespace_declarations = file_scoped:suggestion

Enforce Naming Conventions

# CORRECT: Naming rules enforced as errors
dotnet_naming_rule.interfaces_must_begin_with_i.severity = error
dotnet_naming_rule.types_must_be_pascal_case.severity = error
dotnet_naming_rule.private_fields_must_be_camel_case.severity = warning
# WRONG: Naming rules as suggestions (not enforced)
dotnet_naming_rule.interfaces_must_begin_with_i.severity = suggestion
dotnet_naming_rule.types_must_be_pascal_case.severity = suggestion

NuGet Publishing

Library Projects Include Package Metadata

<!-- CORRECT: Package metadata in .csproj for publishable library -->
<PropertyGroup>
    <PackageId>Acme.Shared.Contracts</PackageId>
    <Version>1.0.0</Version>
    <Description>Shared contracts and DTOs for Acme services</Description>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageReadmeFile>README.md</PackageReadmeFile>
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<!-- WRONG: Missing symbol packages and source link -->
<PropertyGroup>
    <PackageId>Acme.Shared.Contracts</PackageId>
    <Version>1.0.0</Version>
</PropertyGroup>

Non-Publishable Projects Are Marked IsPackable false

<!-- CORRECT: Test and API projects are not packaged -->
<PropertyGroup>
    <IsPackable>false</IsPackable>
</PropertyGroup>
<!-- WRONG: Not setting IsPackable on non-library projects -->
<!-- dotnet pack would create a .nupkg for the API project -->

Target Framework

Use Latest LTS Framework

<!-- CORRECT: Latest LTS -->
<PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<!-- WRONG: End-of-life or preview framework -->
<PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

Libraries May Multi-Target for Compatibility

<!-- CORRECT: Multi-target for broad compatibility -->
<PropertyGroup>
    <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<!-- WRONG: Multi-target including unsupported frameworks -->
<PropertyGroup>
    <TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
</PropertyGroup>

Analyzer Configuration

Analyzers Are Configured in Directory.Build.props

<!-- CORRECT: Analyzers in Directory.Build.props, applied to all projects -->
<ItemGroup>
    <PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" PrivateAssets="all" />
</ItemGroup>
<!-- WRONG: Analyzers added per project -->
<!-- In Catalog.Api.csproj -->
<ItemGroup>
    <PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" />
</ItemGroup>

<!-- In Catalog.Application.csproj -->
<ItemGroup>
    <PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" />
</ItemGroup>

Analyzer Severity in .editorconfig, Not .csproj

# CORRECT: Severity in .editorconfig
[*.cs]
dotnet_diagnostic.CA1062.severity = none
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.IDE0005.severity = warning
<!-- WRONG: Severity in .csproj -->
<PropertyGroup>
    <NoWarn>$(NoWarn);CA1062;CA2007</NoWarn>
</PropertyGroup>

Build Determinism

Enable Deterministic Builds

<!-- CORRECT: Deterministic build in Directory.Build.props -->
<PropertyGroup>
    <Deterministic>true</Deterministic>
    <ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
<!-- WRONG: No determinism settings -->
<!-- Builds may produce different binaries for the same source -->

dotnet format Integration

Format Exclusions Are Applied Consistently

Always exclude generated code from formatting checks:

# CORRECT: Exclude generated code
dotnet format --verify-no-changes --exclude obj/ --exclude Migrations/
# WRONG: No exclusions (fails on EF migrations)
dotnet format --verify-no-changes

.gitignore Essentials

Must Ignore Build Artifacts and IDE Files

# CORRECT: Standard .NET .gitignore
[Bb]in/
[Oo]bj/
TestResults/
.vs/
.idea/
*.user
*.suo
launchSettings.json
# WRONG: Missing critical entries
bin/
obj/
# Missing .vs/, TestResults/, *.user
Stats
Parent Repo Stars0
Parent Repo Forks0
Last CommitFeb 10, 2026