[ ASP .NET Core, EF Core, SQL Server, Docker, SOLID ]

Introduction

In this article, we will explore the process of building a To-Do List application using a RESTful API. The application showcases best practices, adheres to SOLID principles, and incorporates essential design patterns.
The code for the application can be found in the GitHub repository.

Design Patterns and SOLID

Throughout the development of the To-Do List API, adherence to the SOLID principles was a key consideration. By following SOLID, the codebase promotes maintainability and testability. Here’s how the principles were applied:

  1. Single Responsibility Principle (SRP): The code adheres to the SRP by ensuring that each class has a single responsibility. For example, the TdTask class represents a task entity and encapsulates task-related properties and behavior.
  2. Open/Closed Principle (OCP): The code follows the OCP by allowing extension of behavior without modifying existing code. This is evident in the repository pattern implementation, which separates data access logic into the TaskRepository class. Additional repositories can be added without modifying existing code.
  3. Liskov Substitution Principle (LSP): The LSP is upheld by designing the code in a way that derived classes can be substituted for their base classes. The TaskRepository implements the ITaskRepository interface, allowing different repository implementations to be used interchangeably.
  4. Interface Segregation Principle (ISP): The ISP is respected by defining fine-grained interfaces that are specific to the client’s needs. The ITaskRepository interface provides only the necessary methods for interacting with tasks, avoiding unnecessary dependencies.
  5. Dependency Inversion Principle (DIP): The DIP is implemented by utilizing dependency injection throughout the codebase. Constructor injection is used to provide dependencies to classes, promoting loose coupling and testability.

I used some other design patterns as well:

  • Repository Pattern:
    The repository pattern separates the data access logic from the application’s business logic. By implementing the ITaskRepository interface and creating the TaskRepository class, we encapsulate the data access operations for tasks, providing a clean and consistent way to interact with the underlying data store.
  • Dependency Injection (DI):
    The application leverages the concept of dependency injection to achieve loose coupling and enhance testability. The TaskController class receives an instance of the ITaskRepository interface via constructor injection. This allows for easy substitution of the repository implementation and facilitates unit testing by mocking dependencies.
  • Filter Pattern:
    The ValidationFilterAttribute class demonstrates the use of the filter pattern. By implementing the IActionFilter interface, it intercepts HTTP requests to validate the incoming data. It ensures that the task object is not null and validates the model state before executing the corresponding action method.

Endpoints and Operations

The API provides a set of well-defined endpoints that enable users to perform CRUD (Create, Read, Update, Delete) operations on tasks. Here’s an overview of the available endpoints:

  • Create a Task: [POST] /task/create
    This endpoint creates a new task and returns the newly created task ID.
  • Retrieve a Task: [GET] /task/read/{id}
    This endpoint retrieves a task specified by its ID.
  • Update a Task: [PUT] /task/update/{id}
    This endpoint updates an existing task identified by its ID.
  • Delete a Task: [DELETE] /task/delete/{id}
    This endpoint deletes a task based on its ID.
  • Mark a Task as Complete: [PATCH] /task/mark/complete/{id}
    This endpoint marks a task as completed.
  • Mark a Task as Incomplete: [PATCH] /task/mark/incomplete/{id}
    This endpoint marks a task as incomplete.
  • List all Tasks: [GET] /task/list
    This endpoint lists all tasks.
  • List Overdue Tasks: [GET] /task/list/overdue
    This endpoint lists all overdue tasks.
  • List Pending Tasks: [GET] /task/list/pending
    This endpoint lists all pending tasks.

All these endpoints have been documented with ///summary tag, which allows developers to easily navigate and test, especially using Swagger.

Code Highlights

Here are some noteworthy aspects of the code implementation:

  • The TdTask class represents a task entity with properties such as Id, Title, DueDate, and IsCompleted. It incorporates SOLID principles by encapsulating the logic for setting the completion status within the class itself.
    public class TdTask
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Id { get; private set; }
        [StringLength(200)]
        public string Title { get; set; } = String.Empty;
        public DateTime DueDate { get; set; }
        public bool IsCompleted { get; private set; }
        public void SetCompletionStatus(bool isCompleted)
        {
            IsCompleted = isCompleted;
        }
    }

  • The TaskRepository class implements the ITaskRepository interface and provides the data access methods for tasks. It utilizes Entity Framework Core and the repository pattern to interact with the underlying database.
    public class TaskRepository : ITaskRepository
    {
        private readonly DataContext _context;

        public TaskRepository(DataContext context)
        {
            _context = context;
            _context.Database.EnsureCreated();
        }

        public async Task<int> SaveChangesAsync()
        {
            return await _context.SaveChangesAsync();
        }

        public async Task<TdTask> AddAsync(TdTask tdTask)
        {
            _context.TdTasks!.Add(tdTask);
            await _context.SaveChangesAsync();
            return tdTask;
        }
        ...
    }

  • The DataContext class extends the DbContext and represents the database context for the application. It configures the connection string using values from the settings.json file.
    public class DataContext : DbContext
    {
        public DbSet<TdTask>? TdTasks { get; set; }
        public static string GetCurrentConnectionString()
        {
            var config = new ConfigurationBuilder()
                                  .AddJsonFile("settings.json", optional: false)
                                  .Build();

            return config.GetSection("ConnectionString").Value;
        }
        public DataContext(DbContextOptions options) : base(options) { }
    }

  • The TaskControllerTests class demonstrates unit tests for the TaskController class. It utilizes the Moq framework to mock the repository and verifies the behavior of the controller methods.
    public class TaskControllerTests
    {
        private readonly TaskController _taskController;
        private readonly Mock<ITaskRepository> _repository = new();
        public TaskControllerTests()
        {
            _taskController = new TaskController(_repository.Object);
        }

        [Fact]
        public async Task MarkComplete_ValidId_ReturnsOkResponse()
        {
            // Arrange          
            var testGuid = Guid.NewGuid();  
            _repository.Setup(m => m.SetCompletionStatusAsync(testGuid, true));

            // Act
            var response = await _taskController.MarkComplete(testGuid);

            // Assert
            Assert.IsType<OkResult>(response);
        }
        
        ...
    }

Testing Approach

The API includes a comprehensive set of unit tests to ensure the reliability and correctness of the code. The tests were written using the xUnit testing framework and Moq for mocking dependencies. Let’s highlight the testing approach:

  1. Each test method focuses on a specific scenario, covering different aspects of the API endpoints and their expected behavior.
  2. The Mock<T> class from Moq is utilized to create mock instances of the ITaskRepository interface. This allows for isolated testing of the controller without relying on the actual data access layer.
  3. The Arrange-Act-Assert (AAA) pattern is followed in each test method. The necessary objects and dependencies are arranged, the action under test is executed, and the assertions verify the expected outcomes.

Configuration with Docker

To ensure seamless deployment and containerization, the project is configured to run with a Docker image. The provided Dockerfile sets up the necessary environment and dependencies to host the To-Do List API. By leveraging Docker, the application can be easily deployed across different platforms and environments.

Conclusion

In this article, we explored the process of building a To-Do List API with adherence to best practices, including SOLID principles and comprehensive testing. The codebase demonstrates the implementation of design patterns such as the repository pattern and dependency injection. Additionally, Docker configuration ensures easy deployment and portability. By following these best practices, you can create robust and maintainable APIs for managing tasks efficiently.

Go ahead and clone the repository, thanks to Docker, it will just run and work (provided you have Docker installed :))

GitHub: A Simple To Do List API

Leave a reply