Jan 28, 2019

|

by: james

|

Tags: API, RESTful API, Unit Testing

|

Categories: Tech Tips

A Generic Client for RESTful APIs

A Generic Client for RESTful APIs

This morning, our accounting team was doing some regular weekly work bringing in time tracking data from our time tracking tool (we use Toggl) into our back office and invoice systems.  As I was passing by, I thought, “This sure would be easy to do if Toggl has an API we can hit.”  I did a quick search and it turns out that they do!  So, I made it my job to automate as much of this as we could.

First, Make Something That Works

Toggl has a very fully featured API.  For our needs, we just needed to hit a small number of endpoints: a few on their main API to get some high-level IDs for groups and clients and the Summary endpoint on the Reports API to get some aggregate weekly data.

I played around with curl to make some ad-hoc requests to get the client and workspace IDs I needed, which allowed me to focus on what I figured would be the core of what I needed to do, which was get summary data from the Reports API, parse it, do some translation and mapping, and import it into our other systems.

For that I wrote the following bit of code:

        public static string GetSummary(

int workspace_id, DateTime since, DateTime until, string user_agent, string username,

string password)

        {

            using (var client = new HttpClient())

            {

                client.BaseAddress = new System.Uri(“https://toggl.com/reports/api/v2/summary”);

               

                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(

                    “basic”,

                    Convert.ToBase64String(

                        System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format(“{0}:{1}”,

username, password))));

 

                var queryStringParameters = new Dictionary<string, string>

                {

                    { “workspace_id”, workspace_id.ToString() },

                    { “since”, String.Format(“{0:yyyy-MM-dd}”, since) },

                    { “until”, String.Format(“{0:yyyy-MM-dd}”, until) },

                    { “user_agent”, user_agent }

                };

 

                var requestWithQueryStringParameters = “”;

                if (queryStringParameters != null && queryStringParameters.Count > 0)

                {

                    requestWithQueryStringParameters += “?”;

 

                    foreach (var queryStringParameter in queryStringParameters)

                    {

                        if (requestWithQueryStringParameters[requestWithQueryStringParameters.Length – 1]

!= ‘?’)

                        {

                            requestWithQueryStringParameters += “&”;

                        }

                        requestWithQueryStringParameters +=

System.Web.HttpUtility.UrlEncode(queryStringParameter.Key) + “=” +

System.Web.HttpUtility.UrlEncode(queryStringParameter.Value);

                    }

                }

 

                var response = client.GetAsync(requestWithQueryStringParameters).Result;

 

                if (response.IsSuccessStatusCode)

                {

                    using (var reader = new StreamReader(response.Content.ReadAsStreamAsync().Result))

                    {

                        return reader.ReadToEnd();

                    }

                }

            }

            return null;

        }

   

 

Smart, I thought.  This method allows credentials, report parameters, and some other parameters to be passed in allowing us to change those whenever necessary.  The task of exporting this data was already half done (though I still had to write the code to parse that response) and it was a quick implementation to boot.

Next, Don’t Spend Forever Writing It

One of the things that I’ve noticed is that we developers often have a hard time wrapping up implementations because we think of a better way to do it midway through and we essentially start all over.  Which isn’t a big problem except then in that second pass, we think of an even better way midway through and start all over again . . . You see where this is going.  I am a big advocate of iterative development, but that is not iterative development.  It’s infinite development and a great way to get nowhere fast.  Remember that sometimes the brute force implementation is all you will ever need, and it might take you no time at all!

Then, Make It Better

So, as I moved on from writing the code to fetch report data and started on the implementation to get the high-level IDs, it became obvious that a good bit of that code was going to be duplicating the report logic but with a different URL and different query string parameters.

I thought it would now be a good time to write a generic implementation that would work across both the Toggl API and the Reports API.  I also starting thinking about how to parse the responses from all of these calls.  In some cases, like the response from the Summary method, parsing it and mapping into a strongly typed class would be a huge benefit, and in others, just getting the ID would be sufficient, so I refactored the original GetSummary() method above and came up with the following generic implementations:

        public static T Get<T>(string url, Dictionary<string, string> queryStringParameters = null,

            string username = “”, string password = “”)

        {

            var result = GetRaw(url, queryStringParameters, username, password);

            if (result != null)

            {

                return JsonConvert.DeserializeObject<T>(result);

            }

            return default(T);

        }

 

        public static dynamic Get(string url, Dictionary<string, string> queryStringParameters = null,

            string username = “”, string password = “”)

        {

            var result = GetRaw(url, queryStringParameters, username, password);

            if (result != null)

            {

                return JsonConvert.DeserializeObject(result);

            }

            return null;

        }

 

        public static string GetRaw(string url, Dictionary<string, string> queryStringParameters = null,

           string username = “”, string password = “”)

        {

            using (var client = new HttpClient())

            {

                client.BaseAddress = new System.Uri(url);

                if (!string.IsNullOrEmpty(username))

                {

                    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(

                        “basic”,

                        Convert.ToBase64String(

                            System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format(“{0}:{1}”,

                                username, password))));

                }

 

                var requestWithQueryStringParameters = “”;

                if (queryStringParameters != null && queryStringParameters.Count > 0)

                {

                    requestWithQueryStringParameters += “?”;

 

                    foreach (var queryStringParameter in queryStringParameters)

                    {

                        if (requestWithQueryStringParameters[requestWithQueryStringParameters.Length – 1]

                            != ‘?’)

                        {

                            requestWithQueryStringParameters += “&”;

                        }

                        requestWithQueryStringParameters +=

                            System.Web.HttpUtility.UrlEncode(queryStringParameter.Key) + “=” +

                            System.Web.HttpUtility.UrlEncode(queryStringParameter.Value);

                    }

                }

 

                var response = client.GetAsync(requestWithQueryStringParameters).Result;

 

                if (response.IsSuccessStatusCode)

                {

                    using (var reader = new StreamReader(response.Content.ReadAsStreamAsync().Result))

                    {

                        return reader.ReadToEnd();

                    }

                }

            }

 

            return null;

        }

 

As you can see, I implemented a method called GetRaw() to handle the GET operation in a generic way and two methods that would take the string response and either map it into a strongly typed class or hand it over as a dynamic that I could simply dot into — to get an ID, for instance.

Now that the GET operation was done (note that the parsing was done by creating a set of classes whose properties match those in the response and letting the JSON deserializer handle it), I looked ahead to the logic to insert, modify, or delete in the other systems, it looked like a good bet that the logic above could easily be extended to support PUT, POST, and DELETE operations.  So, that’s what I did:

        public static T Post<T>(string url, T t, string username = “”, string password = “”)

        {

            using (var client = new HttpClient())

            {

                client.BaseAddress = new System.Uri(url);

                if (!string.IsNullOrEmpty(username))

                {

                    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(

                        “basic”,

                        Convert.ToBase64String(

                            System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format(“{0}:{1}”,

                            username, password))));

                }

 

                var response = client.PostAsync(url, new StringContent(JsonConvert.SerializeObject(t),

                    Encoding.UTF8, “application/json”)).Result;

                if (response.IsSuccessStatusCode)

                {

                    using (var reader = new StreamReader(response.Content.ReadAsStreamAsync().Result))

                    {

                        return JsonConvert.DeserializeObject<T>(reader.ReadToEnd());

                    }

                }

            }

 

            return default(T);

        }

 

        public static T Put<T>(string url, T t, string username = “”, string password = “”)

        {

            using (var client = new HttpClient())

            {

                client.BaseAddress = new System.Uri(url);

                if (!string.IsNullOrEmpty(username))

                {

                    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(

                        “basic”,

                        Convert.ToBase64String(

                            System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format(“{0}:{1}”,

                            username, password))));

                }

 

                var response = client.PutAsync(url, new StringContent(JsonConvert.SerializeObject(t),

                    Encoding.UTF8, “application/json”)).Result;

                if (response.IsSuccessStatusCode)

                {

                    using (var reader = new StreamReader(response.Content.ReadAsStreamAsync().Result))

                    {

                        return JsonConvert.DeserializeObject<T>(reader.ReadToEnd());

                    }

                }

            }

 

            return default(T);

        }

 

        public static T Delete<T>(string url, string username = “”, string password = “”)

        {

            var result = DeleteRaw(url, username, password);

            if (result != null)

            {

                return JsonConvert.DeserializeObject<T>(result);

            }

            return default(T);

        }

 

        public static dynamic Delete(string url, string username = “”, string password = “”)

        {

            var result = DeleteRaw(url, username, password);

            if (result != null)

            {

                return JsonConvert.DeserializeObject(result);

            }

            return null;

        }

 

        public static string DeleteRaw(string url, string username = “”, string password = “”)

        {

            using (var client = new HttpClient())

            {

                client.BaseAddress = new System.Uri(url);

                if (!string.IsNullOrEmpty(username))

                {

                    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(

                        “basic”,

                        Convert.ToBase64String(

                            System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format(“{0}:{1}”,

                            username, password))));

                }

 

                var response = client.DeleteAsync(url).Result;

 

                if (response.IsSuccessStatusCode)

                {

                    using (var reader = new StreamReader(response.Content.ReadAsStreamAsync().Result))

                    {

                        return reader.ReadToEnd();

                    }

                }

            }

 

            return null;

        }

 

Even as I look at it now, I see some ways that the code above can be improved, but it’s functional, and it solves the problem that I needed to solve.

Finally, Done . . . For Now

I implemented unit tests for all of these methods, so if there’s a new feature that we need to support in the future (and I’m already seeing that having basic auth embedded in each method is something that I’d like to change but am really fighting the urge to make that change until I need it), I can go in and refactor this with a high degree of comfort that the new code won’t break existing functionality:

        [TestMethod]

        public void GetCustomersWithClient()

        {

            dynamic result = SharpEcho.Http.Client.Get(“http://localhost/SharpEcho.Api/api/Customer”,

                null, “username”, “password”);

 

            Assert.IsNotNull(result);

            Assert.IsTrue(result[0].Id > 0);

        }

 

        [TestMethod]

        public void GetCustomerListWithClient()

        {

            var result = SharpEcho.Http.Client.Get<List<Customer>>(

                “http://localhost/SharpEcho.Api/api/Customer”, null, “username”, “password”);

 

            Assert.IsNotNull(result);

            Assert.IsTrue(result.Count > 0);

        }

 

        [TestMethod]

        public void PostCustomerWithClient()

        {

            var customer = new Customer

            {

                Address = “New Address”,

                Name = “New Name”,

                Phone = “New Phone”

            };

 

            var result = SharpEcho.Http.Client.Post<Customer>(

                “http://localhost/SharpEcho.Api/api/Customer”, customer, “username”, “password”);

 

            Assert.IsNotNull(result);

            Assert.IsTrue(result.Id > 0);

            Assert.IsTrue(result.Address == customer.Address);

            Assert.IsTrue(result.Name == customer.Name);

            Assert.IsTrue(result.Phone == customer.Phone);

 

            var getCustomer = SharpEcho.Http.Client.Get<Customer>(

                “http://localhost/SharpEcho.Api/api/Customer/” + result.Id.ToString(), null,

                “username”, “password”);

 

            Assert.IsNotNull(getCustomer);

            Assert.AreEqual(result.Id, getCustomer.Id);

            Assert.AreEqual(result.Address, getCustomer.Address);

            Assert.AreEqual(result.Name, getCustomer.Name);

            Assert.AreEqual(result.Phone, getCustomer.Phone);

        }

 

        [TestMethod]

        public void PutCustomerWithClient()

        {

            var customer = new Customer

            {

                Address = “New Address”,

                Name = “New Name”,

                Phone = “New Phone”

            };

 

            var postCustomer = SharpEcho.Http.Client.Post<Customer>(

                “http://localhost/SharpEcho.Api/api/Customer”, customer, “username”, “password”);

 

            postCustomer.Address = “Updated Address”;

            postCustomer.Name = “Updated Name”;

            postCustomer.Phone = “Upated Phone”;

 

            var result = SharpEcho.Http.Client.Put<Customer>(

                “http://localhost/SharpEcho.Api/api/Customer”, postCustomer, “username”, “password”);

 

            Assert.IsNotNull(result);

            Assert.IsTrue(result.Id == postCustomer.Id);

            Assert.IsTrue(result.Address == postCustomer.Address);

            Assert.IsTrue(result.Name == postCustomer.Name);

            Assert.IsTrue(result.Phone == postCustomer.Phone);

        }

 

        [TestMethod]

        public void DeleteCustomerWithClient()

        {

            var customer = new Customer

            {

                Address = “New Address”,

                Name = “New Name”,

                Phone = “New Phone”

            };

 

            var postCustomer = SharpEcho.Http.Client.Post<Customer>(

                “http://localhost/SharpEcho.Api/api/Customer”, customer, “username”, “password”);

 

            dynamic result = SharpEcho.Http.Client.Delete(“http://localhost/SharpEcho.Api/api/Customer/” +

                postCustomer.Id.ToString(), “username”, “password”);

 

            Assert.IsNotNull(result);

            Assert.AreEqual(result.Id, postCustomer.Id);

        }

 

Even as is, what we have here is a fairly full featured, generic client-side RESTful API library (with covering unit tests) that pairs nicely to an implementation that we showcased in one of our previous articles, “A Generic Approach to Restful APIs,” which describes how to implement the server side in a generic manner. 

With both of these at hand, this gives us a great starting point to ramp up quickly on our next project, and we hope it does the same for you – let us know if it does!