C# / .NET/04core10 min

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 Domain project has no reference to EF Core, ASP.NET or any infrastructure. Business rules are independent of the data access technology. Repository interfaces live in Application, and their implementations in Infrastructure.

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: Nullable and TreatWarningsAsErrors are enabled globally in Directory.Build.props. No project lowers these settings without justification in the PR.

ProjectKnowsDoesn't know
DomainnothingEF, HTTP, DI
ApplicationDomaina specific database/HTTP
InfrastructureDomain, Applicationcontrollers
Apiall

Tip: unit tests target Application and Domain (without infrastructure), and integration tests target Infrastructure and Api with 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.