Celestial Searches Revisited
Sep 23, 2021 – Using TDD to make data access clean and DRY
See the code for this post on GitHub
It's never quite sat well with me how "cowboy-coded" the Celestial Objects Search Function was, as seen in this previous commit of the code. There was SQL just hanging out there with all the other code:
string searchTerm = ((string)req.Query["search"])?.ToUpper();
string queryString = "SELECT TOP 5 * FROM c WHERE CONTAINS(UPPER(c.Name), @term) OR CONTAINS(UPPER(c[\"Common names\"]), @term)";
QueryDefinition queryDef = new QueryDefinition(queryString)
.WithParameter("@term", searchTerm);
...plenty of magic strings to go around...
private static string URL = Environment.GetEnvironmentVariable("AzureCosmosEndpoint");
private static string KEY = Environment.GetEnvironmentVariable("AzureCosmosKey");
// ...
var iterator = cosmosClient
.GetDatabase("StarLog")
.GetContainer("CelestialObjects")
.GetItemQueryIterator<CelestialObject>(queryDef);
...and a bunch of CosmosDB boilerplate that wasn't DRY...
List<CelestialObjectModel> results = new List<CelestialObjectModel>();
while(iterator.HasMoreResults)
{
var result = await iterator.ReadNextAsync();
var mapper = _mapperConfig.CreateMapper();
foreach(var item in result.Resource)
{
results.Add(mapper.Map<CelestialObjectModel>(item));
}
}
Just nasty!
I wanted to clean some of this code up before I started sinking my teeth into saving the user's observations. Dependency injection can be used for configuration values. A repository pattern can be used for abstracting away data access concerns. And most importantly, these kinds of changes can make unit testing and test-driven development way more feasible.
I used a couple Azure code samples as a guide to how I might want to lay things out. I ended up with sort of a hybrid approach between these two samples.
I've got a CosmosDb Repository factory that uses dependency injection for the CosmosClient
and database config options...
public CosmosDbRepositoryFactory(
IOptions<CosmosDbOptions> cosmosDbOptions,
CosmosClient client)
{
_databaseName = cosmosDbOptions.Value?.DatabaseName
?? throw new ArgumentException(nameof(CosmosDbOptions.DatabaseName));
_client = client;
}
...which returns an instance of the CosmosDB Repository class for common data operations:
public class CosmosDbRepository : ICosmosDbRepository
{
private Container _container;
public CosmosDbRepository(
CosmosClient dbClient,
string databaseName,
string containerName)
{
_container = dbClient.GetContainer(databaseName, containerName);
}
// Data read/write/delete methods below...
All of the code for these classes was written against unit tests first, which was a nice return to form for me. Test-driven development is my default mode for the kind of API work I do at my day job, and it was nice to finally bring some TDD to this project.
I've got a clear runway now for creating API endpoints for CRUD operations on the Observation data. With a test project started, some nice reusable data repository classes, and dependency injection in full swing, I should be able to start writing some tests for the next phase of the project.
StarLog Links
All Posts
- MVP Wrap-Up [11/12/2021]
- Creating and Deleting [10/31/2021]
- The Vertical Slice [10/5/2021]
- Celestial Searches Revisited [9/23/2021]
- Authentication [9/17/2021]
- The SvelteKit Refactor [9/12/2021]
- Celestial Searches [8/29/2021]
- The Carbon Refactor [8/20/2021]
- Proof of Concept [6/16/2021]
- Laying the Foundation [5/24/2021]
- The Project Roadmap [5/19/2021]
- Introduction [5/18/2021]