.NET Project Structure
Introduction
Understanding how .NET projects are organized is essential for any developer working with the .NET ecosystem. A well-structured project makes development more efficient, promotes code reuse, simplifies maintenance, and facilitates collaboration with other developers.
In this guide, we'll explore the typical structure of .NET projects, from solutions and projects to files and folders, giving you a solid foundation for organizing your own .NET applications.
Solutions and Projects
What is a .NET Solution?
A solution (.sln file) in .NET is a container that groups together one or more related projects. Think of it as a workspace for your application.
MySolution.sln
The solution file itself is a text file that stores:
- References to all projects in the solution
- Build configurations (Debug, Release, etc.)
- Solution-wide settings
What is a .NET Project?
A project (.csproj, .fsproj, or .vbproj file) represents a component of your application that compiles into a library or executable. Each project contains:
MyProject.csproj
The project file defines:
- Target framework
- Dependencies and packages
- Compilation settings
- Output type (executable, library, etc.)
Basic Solution Structure
Here's what a typical .NET solution structure might look like:
MySolution/
├── MySolution.sln
├── src/
│ ├── MyProject.Core/
│ │ └── MyProject.Core.csproj
│ ├── MyProject.Infrastructure/
│ │ └── MyProject.Infrastructure.csproj
│ └── MyProject.Web/
│ └── MyProject.Web.csproj
└── tests/
├── MyProject.Core.Tests/
│ └── MyProject.Core.Tests.csproj
└── MyProject.Web.Tests/
└── MyProject.Web.Tests.csproj
Project File Anatomy
Let's examine a simple .NET project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<OutputType>Library</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyProject.Core\MyProject.Core.csproj" />
</ItemGroup>
</Project>
Key components:
- Sdk: Defines the type of project (web, console, library)
- PropertyGroup: Contains project properties like target framework and output type
- ItemGroup: Contains references to packages and other projects
Common Project Types
Console Application
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
</Project>
A simple console application has Exe
as its output type.
Class Library
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
</Project>
Class libraries are reusable components and have no specified output type (defaults to Library
).
Web Application
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
</Project>
Web applications use the Microsoft.NET.Sdk.Web
SDK.
Standard Project Structure
Within a .NET project, files are organized in a specific way:
MyProject/
├── MyProject.csproj
├── Program.cs # Entry point for applications
├── Properties/
│ └── launchSettings.json # Debug and launch configurations
├── Controllers/ # (For web projects) API controllers
├── Models/ # Data models
├── Services/ # Business logic
├── Data/ # Data access
├── wwwroot/ # (For web projects) Static files
│ ├── css/
│ ├── js/
│ └── images/
├── appsettings.json # Configuration
└── appsettings.Development.json # Development configuration
Multi-Project Architecture
In larger applications, you'll typically separate concerns across multiple projects:
Common Multi-Project Structure
MySolution/
├── MySolution.Core/ # Domain models, interfaces
├── MySolution.Infrastructure/ # Data access, external services
├── MySolution.Application/ # Business logic, application services
├── MySolution.API/ # Web API endpoints
└── MySolution.Tests/ # Test projects
Benefits of Multi-Project Architecture
- Separation of Concerns: Each project has a specific responsibility
- Reusability: Core components can be reused across applications
- Testability: Easier to test isolated components
- Maintainability: Changes in one area don't affect others
Practical Example
Let's build a simple task management application with a layered architecture.
First, we create a solution:
dotnet new sln -n TaskManager
Now, add the necessary projects:
dotnet new classlib -n TaskManager.Core
dotnet new classlib -n TaskManager.Infrastructure
dotnet new webapi -n TaskManager.API
dotnet new xunit -n TaskManager.Tests
dotnet sln add TaskManager.Core/TaskManager.Core.csproj
dotnet sln add TaskManager.Infrastructure/TaskManager.Infrastructure.csproj
dotnet sln add TaskManager.API/TaskManager.API.csproj
dotnet sln add TaskManager.Tests/TaskManager.Tests.csproj
Add project references:
dotnet add TaskManager.Infrastructure/TaskManager.Infrastructure.csproj reference TaskManager.Core/TaskManager.Core.csproj
dotnet add TaskManager.API/TaskManager.API.csproj reference TaskManager.Core/TaskManager.Core.csproj
dotnet add TaskManager.API/TaskManager.API.csproj reference TaskManager.Infrastructure/TaskManager.Infrastructure.csproj
dotnet add TaskManager.Tests/TaskManager.Tests.csproj reference TaskManager.Core/TaskManager.Core.csproj
Implementation Example
TaskManager.Core/Models/Task.cs:
namespace TaskManager.Core.Models;
public class Task
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
}
TaskManager.Core/Interfaces/ITaskRepository.cs:
using TaskManager.Core.Models;
namespace TaskManager.Core.Interfaces;
public interface ITaskRepository
{
Task<IEnumerable<Models.Task>> GetAllTasksAsync();
Task<Models.Task?> GetTaskByIdAsync(Guid id);
Task<Models.Task> CreateTaskAsync(Models.Task task);
Task UpdateTaskAsync(Models.Task task);
Task DeleteTaskAsync(Guid id);
}
TaskManager.Infrastructure/Repositories/InMemoryTaskRepository.cs:
using TaskManager.Core.Interfaces;
using TaskManager.Core.Models;
namespace TaskManager.Infrastructure.Repositories;
public class InMemoryTaskRepository : ITaskRepository
{
private readonly List<Task> _tasks = new();
public async Task<IEnumerable<Task>> GetAllTasksAsync()
{
return await System.Threading.Tasks.Task.FromResult(_tasks);
}
public async Task<Task?> GetTaskByIdAsync(Guid id)
{
return await System.Threading.Tasks.Task.FromResult(
_tasks.FirstOrDefault(t => t.Id == id));
}
public async Task<Task> CreateTaskAsync(Task task)
{
task.Id = Guid.NewGuid();
task.CreatedAt = DateTime.UtcNow;
_tasks.Add(task);
return await System.Threading.Tasks.Task.FromResult(task);
}
public async System.Threading.Tasks.Task UpdateTaskAsync(Task task)
{
var existingTask = _tasks.FirstOrDefault(t => t.Id == task.Id);
if (existingTask != null)
{
_tasks.Remove(existingTask);
_tasks.Add(task);
}
await System.Threading.Tasks.Task.CompletedTask;
}
public async System.Threading.Tasks.Task DeleteTaskAsync(Guid id)
{
var task = _tasks.FirstOrDefault(t => t.Id == id);
if (task != null)
{
_tasks.Remove(task);
}
await System.Threading.Tasks.Task.CompletedTask;
}
}
TaskManager.API/Program.cs:
using TaskManager.Core.Interfaces;
using TaskManager.Infrastructure.Repositories;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddSingleton<ITaskRepository, InMemoryTaskRepository>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
TaskManager.API/Controllers/TasksController.cs:
using Microsoft.AspNetCore.Mvc;
using TaskManager.Core.Interfaces;
using TaskManager.Core.Models;
namespace TaskManager.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class TasksController : ControllerBase
{
private readonly ITaskRepository _taskRepository;
public TasksController(ITaskRepository taskRepository)
{
_taskRepository = taskRepository;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Task>>> GetTasks()
{
var tasks = await _taskRepository.GetAllTasksAsync();
return Ok(tasks);
}
[HttpGet("{id}")]
public async Task<ActionResult<Task>> GetTask(Guid id)
{
var task = await _taskRepository.GetTaskByIdAsync(id);
if (task == null)
{
return NotFound();
}
return Ok(task);
}
[HttpPost]
public async Task<ActionResult<Task>> CreateTask(Task task)
{
var createdTask = await _taskRepository.CreateTaskAsync(task);
return CreatedAtAction(nameof(GetTask), new { id = createdTask.Id }, createdTask);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateTask(Guid id, Task task)
{
if (id != task.Id)
{
return BadRequest();
}
await _taskRepository.UpdateTaskAsync(task);
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTask(Guid id)
{
await _taskRepository.DeleteTaskAsync(id);
return NoContent();
}
}
This example demonstrates a clean separation of concerns:
- Core: Contains models and interfaces
- Infrastructure: Contains implementations of interfaces
- API: Exposes functionality through controllers
- Tests: (not shown) would contain tests for each layer
Common Conventions and Best Practices
-
Naming Conventions:
- Use PascalCase for project names
- Name projects according to their purpose (e.g.,
MyApp.Core
,MyApp.Infrastructure
)
-
Project Organization:
- Keep related files in appropriate folders (Models, Services, Controllers)
- Use namespaces that match folder structure
-
Reference Direction:
- References should flow from outer layers to inner layers
- Core projects shouldn't reference implementation projects
-
Configuration Management:
- Keep settings in appropriate appsettings.json files
- Use environment-specific settings files
-
Solution Organization:
- Group related projects under solution folders
- Separate source code from tests
Summary
Understanding .NET project structure is fundamental to building maintainable and scalable applications. The key takeaways are:
- Solutions contain one or more projects
- Projects represent components of your application that compile into libraries or executables
- Project files define metadata, dependencies, and compilation settings
- Multi-project architectures help separate concerns and maintain clean code
- Following conventions makes your code more understandable to other developers
By organizing your .NET applications following these principles, you'll create more maintainable, testable, and extensible software.
Additional Resources
- Microsoft Docs: .NET Project SDKs
- Learn about Clean Architecture
- .NET Project Structure Best Practices
Exercises
-
Create a new .NET solution with multiple projects representing a simple e-commerce application (e-commerce.Core, e-commerce.Infrastructure, e-commerce.Web, and e-commerce.Tests).
-
Add appropriate project references between the projects.
-
Create model classes in the Core project, repository implementations in the Infrastructure project, and controllers in the Web project.
-
Write unit tests in the Tests project for at least one service in your application.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)