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.

© 2022 Andrew Iafrate. Contact me for questions or comments.