In this article, we will create a C# solution structure example that makes our application components more maintainable and testable. We’ll learn to keep the business logic separate from the implementation details so that we can focus on the most important part of the software: the business rules.
In this article, we will illustrate a typical C# solution structure through the example of a simple application that allows users to create and manage tasks. We will start with the entities layer and work our way up to the frameworks and drivers layer. We will implement the project as a web API, but you can follow similar steps to create any other project. For this article, we’re borrowing some Clean Architecture concepts to organize the solution.
What is Clean Architecture
Clean Architecture is a software design philosophy emphasizing the separation of concerns and the independence of implementation details from business logic. It is based on the idea that the most crucial part of a software system is the business rules and that they should be kept separate from the technical details necessary for the system to work. Clean Architecture defines different layers for an application and guides how to organize code and dependencies within those layers. By separating the business logic from the implementation details, Clean Architecture enables developers to adapt to changing requirements and technologies quickly and to replace or update individual components without having to rewrite the entire application.
Clean Architecture is designed to make software systems more maintainable, testable, and flexible. It encourages developers to focus on the business logic of an application and to keep it separate from the technical details that are necessary for the system to work. By doing so, developers can create applications that are easier to understand, modify, and scale.
The main benefits of Clean Architecture include the following:
- Maintainability: Clean Architecture makes it easier to maintain an application by keeping the business logic separate from the implementation details. This means developers can change the implementation details without affecting the business logic and vice versa.
- Testability: Clean Architecture makes it easier to test an application by providing clear boundaries between components. This means that developers can write tests for each component in isolation and can test the interactions between components separately. If you want to boost your C# career, check out our ASP.NET full-stack web development course that also covers test-driven development.
- Flexibility: Clean Architecture makes it easier to adapt to changing requirements and technologies by providing clear boundaries between components. This means that developers can replace or update individual components without having to rewrite the entire application.
In this article, we’ll use some Clean Architecture concepts to structure a simple app.
Steps
To build our solution example, follow these steps:
Step 1: Setup the Solution and Projects
In a terminal or command prompt, use the dotnet
command-line interface to create a new solution and projects:
dotnet new sln -n TaskManager
dotnet new webapi -n TaskManager.Web -o TaskManager.Web
dotnet new classlib -n TaskManager.Application -o TaskManager.Application
dotnet new classlib -n TaskManager.Domain -o TaskManager.Domain
dotnet new classlib -n TaskManager.Infrastructure -o TaskManager.Infrastructure
Then each project to the solution using the dotnet sln
command:
dotnet sln add TaskManager.Web/TaskManager.Web.csproj
dotnet sln add TaskManager.Application/TaskManager.Application.csproj
dotnet sln add TaskManager.Domain/TaskManager.Domain.csproj
dotnet sln add TaskManager.Infrastructure/TaskManager.Infrastructure.csproj
Step 2: Entities
In the Domain
project, create an Entities
folder. Inside the folder, create a new class file named Task.cs
and add the following code:
public class Task
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public bool IsComplete { get; set; }
}
This class represents the business object that represents the core concept of the application.
Step 3: The Repository
This class represents the data transfer object used to transfer data between layers.
- In the
TaskManager.Domain
project, create a new folder namedInterfaces
if it doesn’t exist. - Inside the
Interfaces
folder, create a new interface file namedITaskRepository.cs
and add the following code:
public interface ITaskRepository
{
Task GetById(int id);
void Add(Task task);
void Update(Task task);
void Delete(Task task);
}
This interface defines the methods for accessing the Task
entity in the database.
Step 4: Adding Data Transfer Objects
In the Application
project, create a new folder named DTOs
. Inside the folder, create a new class file named TaskDto.cs
and add the following code:
public class TaskDto
{
public string Title { get; set; }
public string Description { get; set; }
public bool IsComplete { get; set; }
}
Step 5: Use Cases
In the Application
project, create a new Services
folder. Inside the folder, create a new class file named TaskService.cs
and add the following code:
public class TaskService
{
private readonly ITaskRepository _repository;
public TaskService(ITaskRepository repository)
{
_repository = repository;
}
public Task GetById(int id)
{
var task = _repository.GetById(id);
if (task == null)
{
throw new Exception("Task not found.");
}
return task;
}
public Task CreateTask(TaskDto taskDto)
{
var task = new Task { Title = taskDto.Title, Description = taskDto.Description, IsComplete = taskDto.IsComplete };
_repository.Add(task);
return task;
}
public Task UpdateTask(int id, TaskDto taskDto)
{
var task = _repository.GetById(id);
if (task == null)
{
throw new Exception("Task not found.");
}
task.Title = taskDto.Title;
task.Description = taskDto.Description;
task.IsComplete = taskDto.IsComplete;
_repository.Update(task);
return task;
}
public void DeleteTask(int id)
{
var task = _repository.GetById(id);
if (task == null)
{
throw new Exception("Task not found.");
}
_repository.Delete(task);
}
}
This class represents the application-specific business rules and logic.
To fix the reference errors in the TaskManager.Application
project, add a reference to the Domain
layer.
cd TaskManager.Application
dotnet add reference ../TaskManager.Domain
Step 6: Database
Next, add the EntityFramework
nuget package to your TaskManager.Infrastructure
project by running the following command in the terminal in the project’s folder:
dotnet add package Microsoft.EntityFrameworkCore
- In the
TaskManager.Infrastructure
project, create a new class file namedApplicationDbContext.cs
and add the following code:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
public DbSet<Task> Tasks { get; set; }
}
This class represents the database context that will be used to access the database. It includes a DbSet<Task>
property representing the Task
entity in the database.
Step 7: Frameworks and Drivers Layer
In the Infrastructure
project, create a new folder named Repositories
. Inside the folder, create a new class file named TaskRepository.cs
and add the following code:
public class TaskRepository : ITaskRepository
{
private readonly ApplicationDbContext _context;
public TaskRepository(ApplicationDbContext context)
{
_context = context;
}
public Task GetById(int id)
{
return _context.Tasks.Find(id);
}
public void Add(Task task)
{
_context.Tasks.Add(task);
_context.SaveChanges();
}
public void Update(Task task)
{
_context.Tasks.Update(task);
_context.SaveChanges();
}
public void Delete(Task task)
{
_context.Tasks.Remove(task);
_context.SaveChanges();
}
}
This class represents the implementation details, such as the web framework, database access, or other external systems.
To fix the reference errors in the TaskManager.Infrastructure
project, add a reference to the Domain
layer.
cd TaskManager.Infrastructure
dotnet add reference ../TaskManager.Domain
Step 8: Controllers or Presenters
In the Web
project, create a new folder named Controllers
if it doesn’t exist. Inside the folder, create a new class file named TaskController.cs
and add the following code:
[ApiController]
[Route("[controller]")]
public class TaskController : ControllerBase
{
private readonly TaskService _service;
public TaskController(TaskService service)
{
_service = service;
}
[HttpGet("{id}")]
public ActionResult<Task> GetById(int id)
{
var task = _service.GetById(id);
if (task == null)
{
return NotFound();
}
return Ok(task);
}
[HttpPost]
public ActionResult<Task> Create(CreateTaskRequest request)
{
var taskDto = new TaskDto { Title = request.Title, Description = request.Description, IsComplete = request.IsComplete };
var task = _service.CreateTask(taskDto);
return CreatedAtAction(nameof(GetById), new { id = task.Id }, task);
}
[HttpPut("{id}")]
public ActionResult<Task> Update(int id, UpdateTaskRequest request)
{
var taskDto = new TaskDto { Title = request.Title, Description = request.Description, IsComplete = request.IsComplete };
var task = _service.UpdateTask(id, taskDto);
return Ok(task);
}
[HttpDelete("{id}")]
public ActionResult Delete(int id)
{
_service.DeleteTask(id);
return NoContent();
}
}
This class represents the code interacting with the user interface or external systems. If you want to learn more about rest API consider using this article.
Step 9: Wiring up the System
Wire up dependencies: To fix the reference errors and wire up the dependencies in the TaskManager.Web
project, add a reference to all of the other projects.
cd TaskManager.Web
dotnet add reference ../TaskManager.Application
dotnet add reference ../TaskManager.Domain
dotnet add reference ../TaskManager.Infrastructure
Also, add the in-memory implementation of the EF core to the web project:
dotnet add package Microsoft.EntityFrameworkCore.InMemory
Wire up the dependencies in Program.cs
.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("TaskManagerdb"));
builder.Services.AddScoped<ITaskRepository, TaskRepository>();
builder.Services.AddScoped<TaskService>();
To run the app, navigate to the TaskManager.Web
project in a terminal or command prompt, and run the following command:
dotnet run
This will start the web server and make the application available at http://localhost:5000/swagger
where 5000 is your application’s port.
The Role of Projects
We created four projects in the solution:
- TaskManager.Web: The web application project contains the controllers and the startup code.
- TaskManager.Application: The application class library contains the use cases and the business logic.
- TaskManager.Domain: The domain project that contains the entities and the interfaces for the repositories.
- TaskManager.Infrastructure: The infrastructure project that contains the implementation of the repositories and the database context.
By organizing our code in this way, we can easily maintain and test the different components of the application. We can also change the implementation details without affecting the business logic.
Conclusion
Clean Architecture enables us to adapt to changing requirements and technologies quickly. By keeping the business logic separate from the implementation details, we can replace or update individual components without having to rewrite the entire application. This allows us to stay agile and responsive to the needs of our users and the market. By separating the business logic from the implementation details, we can focus on the most important part of the software: the business rules. This approach enables us to adapt to changing requirements and technologies quickly. This enables us to replace or update individual components without having to rewrite the entire application. As a result, it allows us to stay agile and responsive to the needs of our users and the market. By the way, did you know that we offer a unique and powerful online course that boosts your C# career? Check it out here!