Implementing a Dictionary API using real-world best practices using C#
So often people seek knowledge. Today I am seeking the dictionary. I have found an API called Google Dictionary API, even though it is not the offical one. It is a scraped version as at the current time of writing there is not a Google Dicitonary API. So this article is for educational purposes of how to use a real-world API using .NET 5 in C# using real-world design with best practices.
The API’s homepage is at: https://dictionaryapi.dev/ If you are to follow along, I encourage you to read that page well and build a mental model around it. There are other great more feature-packed dictionary APIs but I will use this one because of easyness and not requiring any registeration nor using an API key. The source code of the API can be found here: https://github.com/meetDeveloper/googleDictionaryAPI
Here I expect you to program, know JSON, know what an API is and lets get started.
Basic prototype:
Creating a solution and then a project in terminal (Bash):
dotnet new sln && dotnet new console --name Dictionary.Console
Inside the Main method, will define the url in a string and send a request. Based on the response either the word exists and tell the user that it exists otherwise will just say does not exist.
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(5);
string url = "https://api.dictionaryapi.dev/api/v2/entries/en_US/hello";
var message = new HttpRequestMessage(HttpMethod.Get, url);
message.Content = new StringContent(string.Empty);
var response = httpClient.Send(message);
System.Console.WriteLine(response.IsSuccessStatusCode ? "Exists" : "Does not exist");
Note above I hardcoded the word and the language I want to use. There are other supported languages, however en_US is like the standard English most of the world knows. My experience with this API en_Us won’t work, it must be exact. Language codes are defined at the bottom of the main page at the time of writing of the API.
Observation of how the response is structured:
This is basically it for checking everything working. Now we want to extract the juice out of the API. Looking at the structure of the API. Each word has a ‘word’, ‘phonetics’ and ‘meanings’, I will ignore the ‘phonetics’ for my case. Looking at the ‘meanings’ property, each meaning has a ‘partOfSpeech’ and ‘definitions’ and within each definition exists a ‘definition’, ‘example’ and sometimes ‘synonyms’. I will ignore the ‘synonyms’.
Note also a very crucial thing, the response is a JSON array. This may trip you up when serializing the response, I will take that into account. I have yet to come with a word that returns multiple words but there maybe one.
The API is following standards where all words are lower cased.
Modelling the API:
You could just cast the response of the request to a JsonDocument in the System.Text.Json namespace. However then we would have to do plenty of things manually by looping through the document and the various elements within it. Fortunately we can model bind all the way.
I will follow best practices and design an abstraction of this API, the same way you would do if it were part of a web application. I will abstract all the JSON response except for the ‘synonyms’ and ‘phonetics’ properties as I don’t want them. Will then use the fancy output and print it into the console. And finally will just ask the user of the program to input the language code and word.
Modelling the API request and response in code:
Will first empty the Main method.
Will make a folder/directory called Models and make 3 classes representing the response of a single word. Will start from the deepest layer which is the definition one because of dependencies. Each class will be in it’s own file, it doesn’t really matter but that is a best practice.
The definition:
using System.Text.Json.Serialization;
namespace Dictionary.Console.Models
{
public class WordDefinition
{
[JsonPropertyName("definition")] public string Definition { get; set; }
[JsonPropertyName("example")] public string Example { get; set; }
}
}
The meaning:
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Dictionary.Console.Models
{
public class WordMeaning
{
[JsonPropertyName("partOfSpeech")] public string PartOfSpeech { get; set; }
[JsonPropertyName("definitions")] public IList<WordDefinition> Type { get; set; }
}
}
The word:
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Dictionary.Console.Models
{
public class Word
{
[JsonPropertyName("word")] public string WordName { get; set; }
[JsonPropertyName("meanings")] public IList<WordMeaning> Meanings { get; set; }
}
}
Note that this basically represents what the response is. Also, every single property is decorated with a JsonPropertyName attribute because C# conventions state that properties start with upper case letter (Pascal Casing) as well as having the ability to name the properties in code different names than to JSON. If it were XML we are serializing we would use their appropriate attributes.
Now will create a new folder/directory level with the main class called ‘Api’ to abstract the use of the API and not worry about it’s details. Will create 2 classes to represent the request and response.
The request:
namespace Dictionary.Console.Api
{
public class DictionaryRequest
{
public string LanguageCode { get; set; }
public string Word { get; set; }
}
}
The response:
using Dictionary.Console.Models;
namespace Dictionary.Console.Api
{
public class DictionaryResponse
{
public bool Found { get; set; }
public Word Word { get; set; }
}
}
The reason to abstract the response is because based on info/experience using this specific API, the response may either be existent or not i.e. HTTP 200 or HTTP 404.
This is a best practice because as a user of the API in a larger system you will not need to worry about details of another service.
Modelling the API call:
Now in the same directory Api, will create a class called DictionaryApi as follows:
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using Dictionary.Console.Models;
namespace Dictionary.Console.Api
{
public class DictionaryApi
{
private static string Endpoint = "https://api.dictionaryapi.dev/api/v2/entries";
private readonly HttpClient _httpClient;
public DictionaryApi(HttpClient httpClient)
{
_httpClient = httpClient;
}
public DictionaryResponse GetWord(DictionaryRequest request)
{
// TODO code
}
}
}
The GetWord method will be the method of action. Note I am designing a synchronous API, it is better for it to be asynchronous in a real world UI/web application however this is just a console app.
Before implementing the method, note I am only using a single API, maybe in my application I want to abstract multiple dictionary APIs, let’s say one for scientific jargon, one for literature, one for a specific set of words
and whatever you desire. In that case would use the Strategy Design Pattern and have all classes use a common IDictionaryApi interface. Also use the Factory Design Pattern to create the various abstractions be it at runtime or whenever you desire.
Now will implement the method up to where the word returns a non-success response:
string url = $"{Endpoint}/{request.LanguageCode}/{request.Word}";
var message = new HttpRequestMessage(HttpMethod.Get, url);
message.Content = new StringContent(string.Empty);
var response = _httpClient.Send(message);
if (!response.IsSuccessStatusCode)
return new DictionaryResponse
{
Found = false
};
Above I am short circuiting the method by returning a DictionaryResponse with not found word.
Now will implement the happy path (i.e. the word exists):
var responseData = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
var reader = new Utf8JsonReader(responseData);
var word = JsonSerializer.Deserialize<List<Word>>(ref reader, new JsonSerializerOptions
{
WriteIndented = true,
IncludeFields = true,
})?[0];
return new DictionaryResponse
{
Found = true,
Word = word
};
So first things first, since I am using the synchronous way, an extra line of code is required to convert from pure HttpContent to a model. The word variable is where the word is, note at the end the [0], this is because as I said in the observations section above that the response is a list and I am gurranteed to get at least one object. I don’t need to check. Then just returning the abstracted response.
Consuming the API abstraction:
In the main class hosting the Main method, will declare a shortcut for me as follows:
using C = System.Console;
This is a language feature I like and is handy in this because I will be making a lot of Console.WriteLine calls. With this it will be C.WriteLine.
Will go back to our main method. Create an HttpClient and set it to timeout after 5 seconds.
using var httpClient = new HttpClient();
httpClient.Timeout = TimeSpan.FromSeconds(5);
Pursuing:
var dictionaryApi = new DictionaryApi(httpClient);
var wordRequest = new DictionaryRequest
{
LanguageCode = "en_US",
Word = "hello"
};
var wordResponse = dictionaryApi.GetWord(wordRequest);
if (!wordResponse.Found)
{
C.WriteLine("The word/dictionary does not exist in this dictionary");
return;
}
Above I consume the API with a language code of ‘en_US’ and word ‘hello’. Then I check if the word is not found and do the due dilligence. Note the API itself does not tell me anything if I put the language code to be ‘en_Us’ than if I did a ‘hello1’. In other words, it does not differentiate the error.
Continuing with the happy path now:
C.WriteLine(wordResponse.Word.WordName);
foreach (var meaning in wordResponse.Word.Meanings)
{
C.WriteLine($"- Meaning's part of speech: {meaning.PartOfSpeech}");
foreach (var definition in meaning.Type)
{
C.WriteLine($"-- Definition: {definition.Definition}");
C.WriteLine($"-- Example: {definition.Example}");
}
}
Here I am looping through the word’s parts and doing my due dilligence for the user of this app.
Now I will come back to stop hardcoding the main method and use the arguments from the console. These are the args in the Main method’s argument signature. Will make the first argument to be the language code and the second to be the word’s name. Then will recode the DictionaryRequest as follows:
var wordRequest = new DictionaryRequest
{
LanguageCode = args[0],
Word = args[1]
};
That’s it. Run the application. To run it in terminal as an example:
dotnet run en_GB hello
An important thing to note is that I did not validate the input, the API doesn’t provide me with a way to validate the input dynamically. You can hardcode it in a Validate method. It is up to you.
Remarks:
What we have done together is a common scenario of abstracting external/internal APIs. Validation is really important from the input side.
We expected the API service we are consuming to be 100% available assuming internet availability. This is not how it is done in the real world, depending on internet errors, service downtime or timeouts we have to deal with those issues. Retrying on error is a great strategy to incorporate in the code.
We also made the request from the abstraction to denote a binary response. What if the service was down for a long time, should we expect a different response?
None of the tools I used here are a must, you could have used a different HTTP Client than the one shipped by the .NET 5 framework.
Conclusion:
Throughout this article you may have learned something new about how things are done.
Thanks for reading.