Sunday, August 18, 2024

Mastering SOLID Principles in .NET Core 8.0: A TODO List API Example

Create a .NET Core 8.0 API controller for a TO-DO List application, applying SOLID principles. This example will include the controller, service layer, repository, and models. I'll break it down into multiple parts for clarity.

1. First, let's define our model:

// Models/TodoItem.cs

public class TodoItem

{

    public int Id { get; set; }

    public string Title { get; set; }

    public bool IsCompleted { get; set; }

    public DateTime CreatedAt { get; set; }

}

2. Next, let's define our repository interface:

// Interfaces/ITodoRepository.cs

public interface ITodoRepository

{

    Task<IEnumerable<TodoItem>> GetAllAsync();

    Task<TodoItem> GetByIdAsync(int id);

    Task<TodoItem> CreateAsync(TodoItem todoItem);

    Task UpdateAsync(TodoItem todoItem);

    Task DeleteAsync(int id);

}

3. Now, let's implement the repository:

// Repositories/TodoRepository.cs

public class TodoRepository : ITodoRepository

{

    private readonly List<TodoItem> _todos = new List<TodoItem>();

    private int _nextId = 1;


    public async Task<IEnumerable<TodoItem>> GetAllAsync()

    {

        return await Task.FromResult(_todos);

    }

    public async Task<TodoItem> GetByIdAsync(int id)

    {

        return await Task.FromResult(_todos.FirstOrDefault(t => t.Id == id));

    }

    public async Task<TodoItem> CreateAsync(TodoItem todoItem)

    {

        todoItem.Id = _nextId++;

        todoItem.CreatedAt = DateTime.UtcNow;

        _todos.Add(todoItem);

        return await Task.FromResult(todoItem);

    }

    public async Task UpdateAsync(TodoItem todoItem)

    {

        var index = _todos.FindIndex(t => t.Id == todoItem.Id);

        if (index != -1)

        {

            _todos[index] = todoItem;

        }

        await Task.CompletedTask;

    }

    public async Task DeleteAsync(int id)

    {

        _todos.RemoveAll(t => t.Id == id);

        await Task.CompletedTask;

    }

}

4. Let's create a service interface:

// Interfaces/ITodoService.cs

public interface ITodoService

{

    Task<IEnumerable<TodoItem>> GetAllTodosAsync();

    Task<TodoItem> GetTodoByIdAsync(int id);

    Task<TodoItem> CreateTodoAsync(TodoItem todoItem);

    Task UpdateTodoAsync(int id, TodoItem todoItem);

    Task DeleteTodoAsync(int id);

}


5. Implement the service:

// Services/TodoService.cs

public class TodoService : ITodoService

{

    private readonly ITodoRepository _repository;


    public TodoService(ITodoRepository repository)

    {

        _repository = repository;

    }

    public async Task<IEnumerable<TodoItem>> GetAllTodosAsync()

    {

        return await _repository.GetAllAsync();

    }

    public async Task<TodoItem> GetTodoByIdAsync(int id)

    {

        return await _repository.GetByIdAsync(id);

    }

    public async Task<TodoItem> CreateTodoAsync(TodoItem todoItem)

    {

        return await _repository.CreateAsync(todoItem);

    }

    public async Task UpdateTodoAsync(int id, TodoItem todoItem)

    {

        var existingTodo = await _repository.GetByIdAsync(id);

        if (existingTodo == null)

        {

            throw new KeyNotFoundException("Todo item not found");

        }

        existingTodo.Title = todoItem.Title;

        existingTodo.IsCompleted = todoItem.IsCompleted;

        await _repository.UpdateAsync(existingTodo);

    }

    public async Task DeleteTodoAsync(int id)

    {

        await _repository.DeleteAsync(id);

    }

}

6. Finally, let's create the API controller:

// Controllers/TodoController.cs

[ApiController]

[Route("api/[controller]")]

public class TodoController : ControllerBase

{

    private readonly ITodoService _todoService;


    public TodoController(ITodoService todoService)

    {

        _todoService = todoService;

    }

    [HttpGet]

    public async Task<ActionResult<IEnumerable<TodoItem>>> GetAllTodos()

    {

        var todos = await _todoService.GetAllTodosAsync();

        return Ok(todos);

    }

    [HttpGet("{id}")]

    public async Task<ActionResult<TodoItem>> GetTodoById(int id)

    {

        var todo = await _todoService.GetTodoByIdAsync(id);

        if (todo == null)

        {

            return NotFound();

        }

        return Ok(todo);

    }

    [HttpPost]

    public async Task<ActionResult<TodoItem>> CreateTodo(TodoItem todoItem)

    {

        var createdTodo = await _todoService.CreateTodoAsync(todoItem);

        return CreatedAtAction(nameof(GetTodoById), new { id = createdTodo.Id }, createdTodo);

    }

    [HttpPut("{id}")]

    public async Task<IActionResult> UpdateTodo(int id, TodoItem todoItem)

    {

        try

        {

            await _todoService.UpdateTodoAsync(id, todoItem);

        }

        catch (KeyNotFoundException)

        {

            return NotFound();

        }

        return NoContent();

    }

    [HttpDelete("{id}")]

    public async Task<IActionResult> DeleteTodo(int id)

    {

        await _todoService.DeleteTodoAsync(id);

        return NoContent();

    }

}

7. Don't forget to set up dependency injection in your `Program.cs`:

// Program.cs

var builder = WebApplication.CreateBuilder(args);


// Add services to the container.

builder.Services.AddControllers();

builder.Services.AddEndpointsApiExplorer();

builder.Services.AddSwaggerGen();


// Register your services

builder.Services.AddSingleton<ITodoRepository, TodoRepository>();

builder.Services.AddScoped<ITodoService, TodoService>();

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();

This example demonstrates the following SOLID principles:

1. Single Responsibility Principle: Each class has a single responsibility.

2. Open/Closed Principle: The code is open for extension but closed for modification.

3. Liskov Substitution Principle: We use interfaces that can be implemented by different classes.

4. Interface Segregation Principle: We use specific interfaces rather than one general-purpose interface.

5. Dependency Inversion Principle: High-level modules depend on abstractions, not concrete implementations.

This code provides a complete, SOLID-compliant API for a TODO list application using .NET Core 8.0. It includes error handling, async/await patterns, and follows RESTful API design principles.