Skip to main content

.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:

xml
<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

xml
<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

xml
<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

xml
<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

  1. Separation of Concerns: Each project has a specific responsibility
  2. Reusability: Core components can be reused across applications
  3. Testability: Easier to test isolated components
  4. 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:

bash
dotnet new sln -n TaskManager

Now, add the necessary projects:

bash
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:

bash
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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

  1. Naming Conventions:

    • Use PascalCase for project names
    • Name projects according to their purpose (e.g., MyApp.Core, MyApp.Infrastructure)
  2. Project Organization:

    • Keep related files in appropriate folders (Models, Services, Controllers)
    • Use namespaces that match folder structure
  3. Reference Direction:

    • References should flow from outer layers to inner layers
    • Core projects shouldn't reference implementation projects
  4. Configuration Management:

    • Keep settings in appropriate appsettings.json files
    • Use environment-specific settings files
  5. 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

Exercises

  1. 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).

  2. Add appropriate project references between the projects.

  3. Create model classes in the Core project, repository implementations in the Infrastructure project, and controllers in the Web project.

  4. 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! :)