Monorepository with C#: Code Organization in Practice
A monorepository (monorepo) is an approach to source code management where all projects of a company or team are stored in a single repository. In the world of C# and .NET, this approach is becoming increasingly popular due to simplified collaborative development, unified dependency versions, and centralized CI/CD. However, without proper code organization, a monorepository can turn into chaos. In this article, we will explore practical techniques for organizing C# code in a monorepository: from folder structure to build configuration.
1. Folder Structure: The Foundation of Order
The first step to a successful monorepository is a well-thought-out directory hierarchy. It is recommended to follow a convention where each project resides in a separate folder, and common components are placed in src/shared. An example of a basic structure:
/monorepo├── src/│ ├── shared/│ │ ├── CommonLib/ # Common utilities, DTOs, interfaces│ │ └── LoggingLib/ # Logging│ ├── services/│ │ ├── AuthService/ # Authentication microservice│ │ └── OrderService/ # Order microservice│ └── apps/│ ├── WebApi/ # Main web application│ └── ConsoleApp/ # Console tool├── tests/│ ├── unit/│ │ ├── CommonLib.Tests/│ │ └── AuthService.Tests/│ └── integration/│ └── IntegrationTests/├── docs/├── scripts/├── .gitignore├── Directory.Build.props└── global.jsonKey points:
- src/ — source code for all projects.
- shared/ — libraries used by multiple projects.
- services/ — microservices or modules.
- apps/ — end applications (Web API, Console, Blazor).
- tests/ — tests, isolated from the source code.
This structure makes it easy to find code, avoid circular dependencies, and simplifies CI/CD setup.
2. Managing Projects with Directory.Build.props
In a monorepository, there is often a need to set common settings for all projects: .NET versions, common packages, code analysis rules. Instead of duplicating these in each .csproj, use the Directory.Build.props file in the repository root. Example:
<Project> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup>
<ItemGroup> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.*" /> </ItemGroup></Project>Now each child project automatically inherits these settings. If you need to override them for a specific project, simply add the properties to its .csproj — they take priority.
Additionally, you can create Directory.Build.targets for common build targets (e.g., copying files after the build).
3. Dependency Management: A Centralized Approach
One of the main challenges of a monorepository is package version conflicts. In .NET 8, built-in support for Central Package Management (CPM) was introduced. To use it, create a Directory.Packages.props file in the root:
<Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> </PropertyGroup> <ItemGroup> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageVersion Include="Serilog" Version="3.1.1" /&g