From Concept to Code - Building a Pomo Assistant API
This blog post walks through setting up a .NET 8 minimal API project, using pomos as an example target resource. "Pomos" here refer to blocks of time structured according to the Pomodoro time management technique.
The Pomo Assistant project is available on GitHub here, and the list of commits described in this post is available in the pull request here.
There are countless ways to implement an API. The way described in this post is just one among many. The post presents the first steps in setting up a new project, as such it intentionally excludes databases, deployments, security, and anything else "too complicated".
Pomodoro Technique
The Pomodoro Technique is a time management method that structures time into 25-minute-long blocks of uninterrupted and focused work, followed by 5-minute breaks.
This commit sets up the following class to model these blocks of time - pomos.
public sealed class Pomo(DateTime completedAt, Intent intent)
{
public Guid Id { get; } = Guid.NewGuid();
public DateTime CompletedAt { get; set; } = completedAt;
public Intent Intent { get; set; } = intent;
public string Description { get; set; } = string.Empty;
public int DurationInMinutes => 25;
}
The class uses a primary constructor because completedAt
and intent
do not require any validation. The Description
property has a default value so it can be set outside the constructor.
The Intent
property belongs to an enumeration type. It is one of maintenance, exploration, or exploitation, intended as a broad classification of the pomo time. The terms exploration and exploitation are borrowed from reinforcement learning. Exploration refers to trying out new things. Exploitation refers to perfecting one's strengths. Maintenance describes pomos spent on upkeep activities such as taking a meal, hygiene, and leisure.
GUID stands for globally unique identifier. It's written as "Guid" due to Pascal case naming conventions as described here. According to the documentation, a GUID is a 128-bit integer that can be used across all computers and networks wherever a unique identifier is required.
CRUD Operations
CRUD stands for Create, Read, Update, Delete. It describes operations common to nearly all kinds of stored data.
The first commit sets up the pomo service interface and request/response models. The pomo service manages a collection of pomos and provides CRUD operations to operate on them. The models represent requests and responses to the service.
GetById
and Search
are two types of "Read" operations to retrieve individual pomos and pomos within a time range. Update
and Patch
are two types of "Update" operations. The former performs a full update while the latter updates an arbitrary subset of properties. It's possible to use Patch
here because the Pomo
class has no nullable properties.
The second commit sets up a test suite for the pomo service.
The setup method resets the pomo service before every test. This is necessary because the pomo service is stateful. The setup ensures that all tests start identically and do not interfere with each other.
The AutoData
attribute is part of the AutoFixture
library. It generates objects with random properties, which is useful here because the property values do not matter; only the fact that they change does.
The BeEquivalentTo
method is part of the FluentAssertions
library. In the code below, it ensures that all properties of request
are present in result
and hold the same values. This works because all properties that can be updated (request) are also returned for reading within the PomoModels.ReadModel
class (result).
result.Should().BeEquivalentTo(request);
The third and final commit implements the pomo service and ensures that all tests pass.
Web API
One way to view web APIs is as a wrapper over internal services for communications over the internet. Within the PomoAssistant
application, C# classes access the pomo service via the IPomoService
interface. Externally, other applications access the pomo service via a web API.
The first commit sets up a web API project using the minimal API template. The second commit adds the pomo endpoints.
All methods in the PomoEndpoints
class follow the same four-step pattern:
- Validate the request if necessary.
- Map the request if necessary.
- Call
IPomoService
. - Return success/fail results.
The PomoEndpoints
class avoids reusing the request models defined in PomoModels
in favour of using its own model PomoRequest
and doing the mapping. This approach has two benefits. Firstly, missing non-nullable properties are set to the default value, possibly leading to problems. This solution prevents that by having all properties in the PomoRequest
be nullable and then validating they are non-null. Secondly, the PomoRequest
can be shared between multiple methods: Create, Update, and Patch.
The instruction builder.Services.AddSingleton<IPomoService, PomoService>();
in the Program.cs
file registers the pomo service for dependency injection as a singleton. This kind of service often works as scoped; however, this solution forgoes a database and makes the pomo service double as pomo storage. A scoped service would reset on every HTTP request, which is undesirable here. A singleton resets only when the application restarts.
Conclusion
The result of all the work is a pomo assistant API with the following auto-generated swagger UI accessible via https://localhost:7096/swagger/index.html