Solution and project structure
A well-structured .NET solution is one where dependencies flow in a single direction and the domain layer knows nothing about the database or HTTP. This lesson shows how we structure projects at ProfessNet.
Splitting into projects
A typical service solution is split into layers as separate projects:
Zeus.Inventory.sln
├── src/
│ ├── Zeus.Inventory.Api/ # controllers, Program.cs, DI
│ ├── Zeus.Inventory.Application/ # logic, services, interfaces
│ ├── Zeus.Inventory.Domain/ # entities, domain rules (zero dependencies)
│ └── Zeus.Inventory.Infrastructure/ # EF Core, HTTP clients, repositories
└── tests/
├── Zeus.Inventory.UnitTests/
└── Zeus.Inventory.IntegrationTests/
Direction of dependencies
Dependencies point inward, toward the domain. The domain depends on nothing.
Api → Application → Domain
↑
Infrastructure
ProfessNet standard: the
Domainproject has no reference to EF Core, ASP.NET or any infrastructure. Business rules are independent of the data access technology. Repository interfaces live inApplication, and their implementations inInfrastructure.
Central package management
We keep NuGet package versions in a single Directory.Packages.props file so
all projects use the same versions.
<!-- Directory.Packages.props -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" />
</ItemGroup>
</Project>
In .csproj you then provide only the package name, without the version:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
</ItemGroup>
Shared project settings
We move repeated settings (Nullable, LangVersion, warnings as errors) to
Directory.Build.props at the repository root.
<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
ProfessNet standard:
NullableandTreatWarningsAsErrorsare enabled globally inDirectory.Build.props. No project lowers these settings without justification in the PR.
| Project | Knows | Doesn't know |
|---|---|---|
| Domain | nothing | EF, HTTP, DI |
| Application | Domain | a specific database/HTTP |
| Infrastructure | Domain, Application | controllers |
| Api | all | — |
Tip: unit tests target
ApplicationandDomain(without infrastructure), and integration tests targetInfrastructureandApiwith a real test database.
Layers as separate projects, dependencies pointing inward, versions and settings centralized. Such a solution grows without turning into a tangle of references, and the domain stays clean and testable.