dotnet-project-structure
dotnet-project-structure
Reference guide for modern .NET project structure and solution layout. Use when creating new solutions, reviewing existing structure, or recommending improvements.
Prerequisites: Run [skill:dotnet-version-detection] first to determine TFM and SDK version — this affects which features are available (e.g., .slnx requires .NET 9+ SDK).
Scope
- Solution layout conventions (.sln, src/, tests/)
- Directory.Build.props and Directory.Build.targets shared config
- Central Package Management (CPM) and lock files
- .editorconfig and analyzer configuration
- SourceLink, NuGet Audit, and nuget.config
Out of scope
- Build output organization (UseArtifactsOutput) -- see [skill:dotnet-artifacts-output]
- MSBuild authoring (custom targets, conditions) -- see [skill:dotnet-msbuild-authoring]
Cross-references: [skill:dotnet-project-analysis] for analyzing existing projects, [skill:dotnet-scaffold-project] for generating a new project from scratch, [skill:dotnet-artifacts-output] for centralized build output layout (UseArtifactsOutput).
Recommended Solution Layout
MyApp/
├── .editorconfig
├── .gitignore
├── global.json
├── nuget.config
├── Directory.Build.props
├── Directory.Build.targets
├── Directory.Packages.props
├── MyApp.slnx # .NET 9+ SDK / VS 17.13+
├── src/
│ ├── MyApp.Core/
│ │ └── MyApp.Core.csproj
│ ├── MyApp.Api/
│ │ ├── MyApp.Api.csproj
│ │ ├── Program.cs
│ │ └── appsettings.json
│ └── MyApp.Infrastructure/
│ └── MyApp.Infrastructure.csproj
└── tests/
├── MyApp.UnitTests/
│ └── MyApp.UnitTests.csproj
└── MyApp.IntegrationTests/
└── MyApp.IntegrationTests.csproj
Key principles:
- Separate
src/andtests/directories - One project per concern (Core/Domain, Infrastructure, API/Host)
- Solution file at the repo root
- All shared build configuration at the repo root
Solution File Formats
.slnx (Modern — .NET 9+)
The XML-based solution format is human-readable and diff-friendly. Requires .NET 9+ SDK or Visual Studio 17.13+.
<Solution>
<Folder Name="/src/">
<Project Path="src/MyApp.Core/MyApp.Core.csproj" />
<Project Path="src/MyApp.Api/MyApp.Api.csproj" />
<Project Path="src/MyApp.Infrastructure/MyApp.Infrastructure.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/MyApp.UnitTests/MyApp.UnitTests.csproj" />
<Project Path="tests/MyApp.IntegrationTests/MyApp.IntegrationTests.csproj" />
</Folder>
</Solution>
Convert existing .sln to .slnx:
dotnet sln MyApp.sln migrate
.sln (Legacy — All Versions)
The traditional format remains the fallback for older tooling, CI agents, and third-party integrations that don't support .slnx yet. Keep .sln alongside .slnx during the transition period if needed.
dotnet new sln -n MyApp
dotnet sln add src/**/*.csproj
dotnet sln add tests/**/*.csproj
Directory.Build.props
Shared MSBuild properties applied to all projects in the directory subtree. Place at the repo root.
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-all</AnalysisLevel>
</PropertyGroup>
</Project>
Nested Directory.Build.props
Inner files do not automatically import outer files. To chain them:
<!-- src/Directory.Build.props -->
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
<PropertyGroup>
<!-- src-specific settings -->
</PropertyGroup>
</Project>
Common pattern: separate props for src vs tests:
repo/
├── Directory.Build.props # Shared: LangVersion, Nullable, ImplicitUsings
├── src/
│ └── Directory.Build.props # Imports parent + adds TreatWarningsAsErrors
└── tests/
└── Directory.Build.props # Imports parent + sets IsTestProject
Directory.Build.targets
Imported after project evaluation. Use for:
- Shared analyzer package references
- Custom build targets
- Conditional logic based on project type
<Project>
<!-- Apply analyzers to all projects -->
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" PrivateAssets="all" />
</ItemGroup>
</Project>
Central Package Management (CPM)
CPM centralizes all NuGet package versions in Directory.Packages.props at the repo root. Individual .csproj files reference packages without a Version attribute.
Directory.Packages.props
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<!-- Shared dependencies -->
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<!-- Test dependencies -->
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
</ItemGroup>
</Project>
Project File with CPM
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<!-- No Version attribute — managed centrally -->
<PackageReference Include="Microsoft.Extensions.Logging" />
</ItemGroup>
</Project>
Version Overrides
When a specific project needs a different version (rare), use VersionOverride:
<PackageReference Include="Newtonsoft.Json" VersionOverride="13.0.3" />
Flag version overrides during code review — they defeat the purpose of CPM.
.editorconfig
Place at the repo root to enforce consistent code style across all editors and the build.
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,xml,json,yml,yaml}]
indent_size = 2
[*.cs]
# Namespace declarations
csharp_style_namespace_declarations = file_scoped:warning
# Braces
csharp_prefer_braces = true:warning
# var preferences
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
# Access modifiers
dotnet_style_require_accessibility_modifiers = always:warning
# Pattern matching
csharp_style_prefer_pattern_matching = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion
# Null checking
csharp_style_prefer_null_check_over_type_check = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
# Expression-level preferences
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = true:suggestion
# Using directives
csharp_using_directive_placement = outside_namespace:warning
dotnet_sort_system_directives_first = true
# Naming conventions
dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields
dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_underscore
dotnet_naming_rule.private_fields_should_be_camel_case.severity = warning
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.camel_case_underscore.required_prefix = _
dotnet_naming_style.camel_case_underscore.capitalization = camel_case
See [skill:dotnet-add-analyzers] for full analyzer rule configuration.
global.json
Pin the SDK version for reproducible builds:
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestPatch"
}
}
Roll-forward policies:
latestPatch— allow patch updates only (recommended for CI)latestFeature— allow feature-band updates within the major versionlatestMajor— use whatever is installed (development convenience, not for CI)disable— exact version only
nuget.config
Configure package sources and security:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
The <clear /> + explicit sources + <packageSourceMapping> pattern prevents supply-chain attacks by ensuring packages only come from expected sources.
For private feeds, map internal package prefixes exclusively to the private source:
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="internal" value="https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="internal">
<package pattern="MyCompany.*" />
</packageSource>
</packageSourceMapping>
NuGet uses most-specific-pattern-wins precedence: MyCompany.Foo matches MyCompany.* (internal) over * (nuget.org), so internal packages restore exclusively from the private feed. This prevents dependency confusion attacks — an attacker cannot squat MyCompany.Foo on nuget.org because NuGet will never look there for packages matching MyCompany.*.
Do not map the same prefix to multiple sources unless you trust both — that defeats the protection.
NuGet Audit
.NET 9+ enables NuGetAudit by default, which checks for known vulnerabilities during restore. Configure the severity threshold:
<!-- In Directory.Build.props -->
<PropertyGroup>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel>
<NuGetAuditMode>all</NuGetAuditMode> <!-- audit direct + transitive -->
</PropertyGroup>
Lock Files
Enable deterministic restores with lock files:
<!-- In Directory.Build.props -->
<PropertyGroup>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
This generates packages.lock.json per project. Commit these files. In CI, restore with --locked-mode:
dotnet restore --locked-mode
SourceLink and Deterministic Builds
For libraries published to NuGet:
<!-- In Directory.Build.props -->
<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>embedded</DebugType>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="all" />
</ItemGroup>
Key properties:
PublishRepositoryUrl— includes the repo URL in the NuGet packageEmbedUntrackedSources— embeds generated source filesDebugType=embedded— PDB embedded in the assembly (no separate symbol package needed)ContinuousIntegrationBuild— enables deterministic paths (only in CI to avoid breaking local debugging)
References
More from novotnyllc/dotnet-artisan
dotnet-csharp
Baseline C# skill loaded for every .NET code path. Guides language patterns (records, pattern matching, primary constructors, C# 8-15), coding standards, async/await, DI, LINQ, serialization, domain modeling, concurrency, Roslyn analyzers, globalization, native interop (P/Invoke, LibraryImport, ComWrappers), WASM interop (JSImport/JSExport), and type design. Spans 25 topics. Do not use for ASP.NET endpoint architecture, UI framework patterns, or CI/CD guidance.
127dotnet-ui
Builds .NET UI apps across Blazor (Server, WASM, Hybrid, Auto), MAUI (XAML, MVVM, Shell, Native AOT), Uno Platform (MVUX, Extensions, Toolkit), WPF (.NET 8+, Fluent theme), WinUI 3 (Windows App SDK, MSIX, Mica/Acrylic, adaptive layout), and WinForms (high-DPI, dark mode) with JS interop, accessibility (SemanticProperties, ARIA), localization (.resx, RTL), platform bindings (Java.Interop, ObjCRuntime), and framework selection. Spans 20 topic areas. Do not use for backend API design or CI/CD pipelines.
99dotnet-api
Builds ASP.NET Core APIs, EF Core data access, gRPC, SignalR, and backend services with middleware, security (OAuth, JWT, OWASP), resilience, messaging, OpenAPI, .NET Aspire, Semantic Kernel, HybridCache, YARP reverse proxy, output caching, Office documents (Excel, Word, PowerPoint), PDF, and architecture patterns. Spans 32 topic areas. Do not use for UI rendering patterns or CI/CD pipeline authoring.
90dotnet-testing
Defines .NET test strategy and implementation patterns across xUnit v3 (Facts, Theories, fixtures, IAsyncLifetime), integration testing (WebApplicationFactory, Testcontainers), Aspire testing (DistributedApplicationTestingBuilder), snapshot testing (Verify, scrubbing), Playwright E2E browser automation, BenchmarkDotNet microbenchmarks, code coverage (Coverlet), mutation testing (Stryker.NET), UI testing (page objects, selectors), and AOT WASM test compilation. Spans 13 topic areas. Do not use for production API architecture or CI workflow authoring.
86dotnet-advisor
Routes .NET/C# requests to the correct domain skill and loads coding standards as baseline for all code paths. Determines whether the task needs API, UI, testing, devops, tooling, or debugging guidance based on prompt analysis and project signals, then invokes skills in the right order. Always invoked after [skill:using-dotnet] detects .NET intent. Do not use for deep API, UI, testing, devops, tooling, or debugging implementation guidance.
60dotnet-debugging
Debugs Windows and Linux/macOS applications (native, .NET/CLR, mixed-mode) with WinDbg MCP (crash dumps, !analyze, !syncblk, !dlk, !runaway, !dumpheap, !gcroot, BSOD), dotnet-dump, lldb with SOS, createdump, and container diagnostics (Docker, Kubernetes). Hang/deadlock diagnosis, high CPU triage, memory leak investigation, kernel debugging, and dotnet-monitor for production. Spans 17 topic areas. Do not use for routine .NET SDK profiling, benchmark design, or CI test debugging.
57