I'm creating a wrapper library for an API, and I hit a block on how to redesign it. The entire library is static, and I'd like to change it to make it more testable.
The library's goal is to make rest api requests and return C# objects. There is a networking class, which makes the requests. And there is a class for each endpoint which abstracts the parameters for the request and supplies the relative url.
Networking class: build url, create headers, and return results from request
public class APIClient{
private static string url = null;
private static string apiKey = null;
public static void Initialize(string key,string url)
{
APIClient.url = url;
APIClient.apiKey = key;
}
public static async Task<T> GetAsync<T>(string path, Dictionary<string, string> parameters)
{
var client = CreateHttpClientFromApiCredentials();
//build url with parameters from the dictionary
var builder = new UriBuilder("", path);
builder.Port = -1;
if (parameters != null)
{
var query = HttpUtility.ParseQueryString("");
foreach (var key in parameters.Keys)
query[key] = parameters[key];
builder.Query = query.ToString();
}
var response = await client.GetAsync(builder.ToString());
if (!response.IsSuccessStatusCode)
throw new HttpException((int)response.StatusCode, response.ReasonPhrase);
//evaluate API specific data
return await EvaluateResponseContent<T>(response);
}
}
Sample endpoint class: provide relative url and arguments to networking class
public class APIObject
{
public static async Task<List<APIObject>> Get()
{
return await APIClient.GetAsync<List<APIObject>>("objectendpoint",null);
}
}
This design allows for very simple use, with calls like APIObject.Get()
.
With that said, I have a redesign in mind that will maintain the simplicity of the static functions while also allowing for easy testing. The APIClient will be refactored to be an object, and each of the API object classes will be a nested class inside the APIClient.
A brief example (untested):
public class APIClient
{
public ApiObjectSender ApiObjectEndpoint;
public APIClient() {
ApiObjectEndpoint = new ApiObjectSender(this);
}
public class ApiObjectSender
{
APIClient apiClient;
public ApiObjectSender(APIClient cl)
{
apiClient = cl;
}
public List<APIObject> Get()
{
return apiClient.Get("apiobjectendpoint");
}
}
}
This allows for similar function calls, like clientVar.ApiObjectEndpoint.Get()
. However, I can't help but feel like this approach is... janky.