.NET Project Structure Explained
- ShiftQuality Contributor
- Mar 30
- 6 min read
You can write C#. You understand variables, types, methods, and classes. But the moment you open a real .NET codebase, you are looking at a wall of folders, XML files, and naming conventions that nobody explained. The code makes sense. The structure does not.
That is not a knowledge gap in programming. It is a knowledge gap in how .NET organizes work. And it matters, because a project with bad structure becomes expensive to maintain the moment a second developer touches it.
Solutions and Projects: The Two Levels
Every .NET codebase has two fundamental organizational units.
A solution (.sln) is the top-level container. It is a file that lists which projects belong together and how they relate. It does not contain code. It contains references. Think of it as a table of contents.
A project (.csproj for C#, .fsproj for F#) is the unit of code. Each project compiles into a single output — a DLL (library) or an EXE (application). A project has its own dependencies, its own build settings, and its own namespace.
One solution contains one or more projects. A small application might have a single project. An enterprise system might have twenty.
MyApp.sln ← the solution file
├── src/
│ ├── MyApp.Api/ ← project: the web API
│ │ └── MyApp.Api.csproj
│ ├── MyApp.Core/ ← project: business logic
│ │ └── MyApp.Core.csproj
│ └── MyApp.Data/ ← project: data access
│ └── MyApp.Data.csproj
└── tests/
├── MyApp.Core.Tests/ ← project: tests for business logic
│ └── MyApp.Core.Tests.csproj
└── MyApp.Api.Tests/ ← project: tests for the API
└── MyApp.Api.Tests.csproj
This is not arbitrary. Each project exists for a reason.
Why Multiple Projects? Separation of Concerns
A beginner instinct is to put everything in one project. It compiles. It runs. Why split it up?
Because software changes. And when it changes, you need to change the right thing without breaking everything else.
MyApp.Api handles HTTP requests and responses. It knows about routes, controllers, and serialization. It does not know how data is stored.
MyApp.Core contains business logic — the rules, calculations, and domain models that define what the application actually does. It has no dependency on the web framework or the database. This is deliberate. Business logic should work regardless of how it is delivered or where its data comes from.
MyApp.Data handles database access. Entity Framework, SQL queries, repository implementations — all of it lives here. If you swap from SQL Server to PostgreSQL, this is the only project that changes.
MyApp.Core.Tests and MyApp.Api.Tests contain automated tests. They are separate projects because tests should not ship with your production code. They add dependencies (test frameworks, mocking libraries) that have no business in a deployed application.
The dependency flow is intentional: Api depends on Core and Data. Core depends on nothing. Data depends on Core (it implements interfaces that Core defines). Tests depend on whatever they are testing.
When you enforce these boundaries at the project level, the compiler enforces them for you. A developer cannot accidentally make the business logic depend on the web framework — it will not compile.
Namespaces: Organizing Code Within a Project
Projects organize code at the build level. Namespaces organize code at the logical level.
A namespace is a grouping of related classes, interfaces, and types. By convention, the namespace matches the folder structure within the project.
MyApp.Core/
├── Models/
│ ├── Customer.cs ← namespace: MyApp.Core.Models
│ └── Order.cs ← namespace: MyApp.Core.Models
├── Services/
│ ├── OrderService.cs ← namespace: MyApp.Core.Services
│ └── PricingService.cs ← namespace: MyApp.Core.Services
└── Interfaces/
└── IOrderRepository.cs ← namespace: MyApp.Core.Interfaces
Each file declares its namespace at the top:
namespace MyApp.Core.Services;
public class OrderService
{
// ...
}
When you need a type from another namespace, you add a using directive:
using MyApp.Core.Models;
using MyApp.Core.Interfaces;
Namespaces prevent name collisions. Two projects can both have a class called Configuration without conflict because they live in different namespaces. More importantly, namespaces communicate intent. When you see MyApp.Core.Services.PricingService, you know where that class lives and what layer it belongs to without opening the file.
The Typical Enterprise Layout
The example above is not invented. It is the standard pattern you will see in professional .NET codebases, with minor variations. The general structure follows a layered architecture:
MySolution.sln
├── src/
│ ├── MyApp.Api/ ← Presentation layer (HTTP, controllers)
│ ├── MyApp.Core/ ← Domain/business layer (logic, models, interfaces)
│ ├── MyApp.Data/ ← Infrastructure layer (database, external services)
│ └── MyApp.Shared/ ← Cross-cutting concerns (DTOs, utilities, constants)
├── tests/
│ ├── MyApp.Core.Tests/ ← Unit tests
│ ├── MyApp.Api.Tests/ ← Integration tests
│ └── MyApp.Data.Tests/ ← Data layer tests
├── .gitignore
├── Directory.Build.props ← Shared build properties for all projects
└── README.md
Directory.Build.props is a file that MSBuild picks up automatically. It applies shared settings — target framework, common package versions, code analysis rules — to every project in the solution. Instead of repeating the same configuration in five .csproj files, you define it once.
The src/ and tests/ folders are not required by .NET tooling. They are a convention that keeps things navigable when a solution grows past a handful of projects.
NuGet Packages: The .NET Package Ecosystem
No developer writes everything from scratch. NuGet is .NET's package manager — the equivalent of npm for JavaScript or pip for Python. It hosts over 400,000 packages covering everything from JSON serialization to logging frameworks to cloud SDKs.
Modern .NET projects use PackageReference format, where dependencies are declared directly in the .csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
</ItemGroup>
</Project>
You add packages from the command line:
dotnet add package Serilog
The older packages.config format stored dependencies in a separate XML file. You will encounter it in legacy .NET Framework projects, but all modern .NET projects use PackageReference. If you are starting new work, you are already on PackageReference by default.
Packages restore automatically when you build. The dotnet restore command downloads them to a global cache on your machine. They are not stored inside your project — which brings us to the next point.
bin/ and obj/ Folders: Build Output
Every .NET project generates two folders when you build:
bin/ (binary) contains the compiled output. Your DLLs, EXEs, and any files needed to run the application. The subfolder structure reflects your build configuration — bin/Debug/net9.0/ for a debug build, bin/Release/net9.0/ for a release build.
obj/ (object) contains intermediate build artifacts. MSBuild uses this folder to track what has changed between builds so it can do incremental compilation. You never need to look inside this folder. It exists for the build system.
Both folders are generated from your source code and packages. They are derived artifacts, not source artifacts. Committing them to version control would be pointless — they can be regenerated by anyone who has the source code — and harmful, because they contain machine-specific paths and binary files that bloat your repository and cause merge conflicts.
If your build ever gets into a weird state, delete both folders and rebuild:
dotnet clean
dotnet build
Or simply delete the bin/ and obj/ directories manually. The next dotnet build regenerates them from scratch.
.gitignore for .NET: What to Exclude
Every .NET repository needs a .gitignore file that excludes build output and IDE-specific files. Here are the entries that matter most:
# Build output
bin/
obj/
# NuGet packages (restored on build, not committed)
*.nupkg
packages/
# User-specific IDE files
*.user
*.suo
.vs/
# Rider
.idea/
# macOS
.DS_Store
The dotnet new gitignore command generates a comprehensive .gitignore for .NET projects. Use it. The default template covers edge cases you would not think to exclude on your own.
The principle is straightforward: commit source code, configuration, and project files. Do not commit anything that can be generated from those inputs. Your repository stays clean, your diffs stay readable, and your teammates do not inherit your local build artifacts.
Putting It Together
Run these commands and you have a structured solution ready for real development:
dotnet new sln -n MyApp
mkdir src tests
dotnet new webapi -n MyApp.Api -o src/MyApp.Api
dotnet new classlib -n MyApp.Core -o src/MyApp.Core
dotnet new classlib -n MyApp.Data -o src/MyApp.Data
dotnet new xunit -n MyApp.Core.Tests -o tests/MyApp.Core.Tests
dotnet sln add src/MyApp.Api src/MyApp.Core src/MyApp.Data tests/MyApp.Core.Tests
dotnet add src/MyApp.Api reference src/MyApp.Core src/MyApp.Data
dotnet add src/MyApp.Data reference src/MyApp.Core
dotnet add tests/MyApp.Core.Tests reference src/MyApp.Core
dotnet new gitignore
Twelve commands. You now have a multi-project solution with proper separation of concerns, project references enforcing dependency direction, a test project, and a .gitignore. From here, you write code — not boilerplate.
The Takeaway
This is the final post in the Getting Started with .NET learning path. You now know what .NET is, how to write C#, and how real .NET codebases are organized. The structure is not ceremony. It is how teams maintain software over years without the codebase collapsing under its own weight.
The information problem with .NET project structure is not that it is complicated. It is that nobody tells you why it is set up this way. Solutions group projects. Projects enforce boundaries. Namespaces organize code within those boundaries. Build output gets excluded from version control. Every convention exists to prevent a specific category of pain that shows up when projects grow.
Where you go from here depends on what you want to build. Some paths worth exploring:
Building Web APIs with ASP.NET Core — Take the structure you learned here and build something that handles real HTTP requests, talks to a database, and returns JSON.
Testing Fundamentals — You have a test project in your solution. Learn how to fill it with tests that actually catch bugs before your users do.
DevOps and Deployment — Your code compiles locally. Learn how to get it running on a server, in a container, or in the cloud.
The foundation is in place. Build on it.



Comments