The OSU Developer Portal (https://developer.oregonstate.edu) currently has two APIs available, Directory (for people) and Location (for campus locations). This post describes my experience developing a “hello world” framework with the Location API. I got excellent reuse of the framework code when also experimenting with the Directory API.
I logged on to the OSU Developer Portal, registered a new app, and got the Consumer Key and Consumer Secret strings that are required to make calls to the API.
A Consumer Key looks something like m5l2jS54r7XqkkvJVovdpUY1o4DMl0la and a Consumer Secret like koMYj5HVd9m963a8. Not the actual ones! Get yer own!
Calling an OSU API has two steps:
- An HTTP POST to the getAccessToken method, with Consumer Key and Consumer Secret, to get an access token.
- An HTTP GET to to the desired API method, here getLocations, with the access token obtained above and any optional parameters, here a q query string.
For example, calling getLocations with the query q=kerr returns this JSON:
{
“links”:{
“self”:”https://api.oregonstate.edu/v1/locations?q=kerr&page[number]=1&page[size]=10″,
“first”:”https://api.oregonstate.edu/v1/locations?q=kerr&page[number]=1&page[size]=10″,
“last”:”https://api.oregonstate.edu/v1/locations?q=kerr&page[number]=1&page[size]=10″,
“prev”:null,
“next”:null
},
“data”:[
{
“id”:”2e9ee2d06066654f61a178560c2c137a”,
“type”:”locations”,
“attributes”:{
“name”:”Kerr Administration Building”,
“abbreviation”:”KAd”,
“latitude”:”44.5640654089″,
“longitude”:”-123.274740377″,
“summary”:”Kerr houses OSU’s top administrative offices and many services for students, including admissions, financial aid, registrar’s office and employment services. If you’d like a student-led campus tour, stop in 108 Kerr to make arrangements.”,
“description”:null,
“address”:”1500 SW Jefferson Avenue.”,
“city”:”Corvallis”,
“state”:”OR”,
“zip”:null,
“county”:null,
“telephone”:null,
“fax”:null,
“thumbnails”:[
“http://oregonstate.edu/campusmap/img/kad001.jpg”
],
“images”:[
null
],
“departments”:null,
“website”:”http://oregonstate.edu/campusmap/locations=766″,
“sqft”:null,
“calendar”:null,
“campus”:”corvallis”,
“type”:”building”,
“openHours”:{
}
},
“links”:{
“self”:”https://api.oregonstate.edu/v1/locations/2e9ee2d06066654f61a178560c2c137a”
}
}
]
}
I used the Microsoft ASP.NET Web API 2.2 Client Libraries NuGet package. This package has features to automagically deserialize JSON into plain ol’ class objects.
These are the class objects. .NET “generic” classes play an important role in the overall solution. Generic classes have a type parameter appended to their name, here <T>, where T is a naming convention used widely by .NET developers; wherever the type parameter (“T”) shows up in the class body is where the generic replacement is propagated when the type is constructed.
namespace JsonDataTransferObjects { //first a few framework classes that can be reused later for other APIs... //root of all JSON returned by the OSU APIs. //note how the "links" and "data" fields // correspond with items of the same name in the JSON. public class RootObject<T> { public Links links { get; set; } public List<T> data { get; set; } } public class Links { public string self { get; set; } public string first { get; set; } public string last { get; set; } public string prev { get; set; } public string next { get; set; } } //Abstract (must inherit) class that corresponds to "id", // "type" and "attributes" of root object JSON public abstract class ObjectData<T> { public string id { get; set; } public string type { get; set; } public T attributes { get; set; } } //now classes for the solution at hand... //constructed type: class declared from a generic type // by supplying type arguments for its type parameters. // now we will be able to work with RootObject<LocationData>, // and the List in the "data" field will be of type LocationData public class LocationData : OregonState.Api.JsonDataTransferObjects.ObjectData<LocationAttributes> { } public class LocationAttributes { public string name { get; set; } public string abbreviation { get; set; } public string latitude { get; set; } public string longitude { get; set; } public string summary { get; set; } public string description { get; set; } public string address { get; set; } public string city { get; set; } public string state { get; set; } public string zip { get; set; } public string county { get; set; } public string telephone { get; set; } public string fax { get; set; } public List<string> thumbnails { get; set; } public List<string> images { get; set; } public object departments { get; set; } public string website { get; set; } public string sqft { get; set; } public string calendar { get; set; } public string campus { get; set; } public string type { get; set; } public LocationOpenHours openHours { get; set; } } public class LocationOpenHours { //todo: not sure what attribute name is } }
Now, just enough code to get this test to pass (which it does!). Note the async and await keywords used for asynchronous programming in the .NET framework.
[TestClass()] public class LocationServiceIntegrationTests { //initialize API service client, here constructed to handle the type LocationData private static readonly ApiServiceClient<LocationData> myServiceClient = MerthjTestApp1ApiServiceClientFactory.CreateLocationDataApiServiceClient(); //gotcha: even unit tests, which normally do not return anything, // must now be "async" and return a "Task" // because of "await" keyword [TestMethod()] async Task LocationServiceClient_Should_ReturnCorrectResultForQueryOnKerr() { RootObject<LocationData> result = await myServiceClient.Request("?q=kerr"); Assert.AreEqual("Kerr Administration Building", result.data.Item[0].attributes.name); } }
Use factory pattern to construct the API client, because this is a complex process, and one that would lead to code duplication if simply NEW-ing the client in code.
public sealed class MerthjTestApp1ApiServiceClientFactory { //todo: move to configuration // (here only because I liked to see the actual address) const string ApiOregonstateBaseAddressUriText = "https://api.oregonstate.edu"; public static ApiServiceClient<LocationData> CreateLocationDataApiServiceClient() { //pass in a new LocationService, a service which makes actual calls // to the API (but we could also use "mock" service here...) //note use of Visual Studio's "My.Resources" to store Consumer Key and Consumer Secret // in a resource file; resource file SHOULD NOT EVER be checked in // to revision control, or your secrets are out there for all to see. return new ApiServiceClient<LocationData>(new LocationService(), ApiOregonstateBaseAddressUriText, "v1/locations", My.Resources.OSU_Developer_Portal_Secrets_Resource.ConsumerKey, My.Resources.OSU_Developer_Portal_Secrets_Resource.ConsumerSecret); } }
An API service client is responsible for managing the access token required before each API call.
public class ApiServiceClient<T> { private readonly IApiService<T> myService; private readonly string myResourceRelativeUriText; private readonly string myBaseUriText; private readonly string myConsumerKey; private readonly string myConsumerSecret; //Lazy object is not constructed until its ".Value" method // is called (a singleton) (see Me.CreateRequestAsync). // Not thread safe! private Lazy<Task<string>> myAccessToken = new Lazy<Task<string>>(new System.EventHandler(this.GetAccessTokenAsync)); //no-arg constructor is private so clients may not use it private ApiServiceClient() { } //constructor required for use by clients public ApiServiceClient(IApiService<T> service, string baseAddressUriText, string resourceRelativeUriText, string consumerKey, string consumerSecret) { // todo: validate parameters this.myService = service; this.myResourceRelativeUriText = resourceRelativeUriText; this.myBaseUriText = baseAddressUriText; this.myConsumerKey = consumerKey; this.myConsumerSecret = consumerSecret; } //use the service to get the access token async Task<string> GetAccessTokenAsync() { Debug.WriteLine("Entering GetAccessTokenAsync"); return await this.myService.GetAccessTokenAsync(this.myBaseUriText, this.myResourceRelativeUriText, this.myConsumerKey, this.myConsumerSecret); } async Task<ApiRequest> CreateRequestAsync(string query) { return new ApiRequest { //actually fetch the access token here! AccessToken = await this.myAccessToken.Value; BaseAddressUriText = this.myBaseUriText; ResourceRelativeUriText = this.myResourceRelativeUriText; Query = query; } } //clients can request API result as raw JSON string for troubleshooting... async Task<string> RequestAsString(string query) { return await this.myService.GetContentAndReadAsStringAsync(await this.CreateRequestAsync(query)); } //most clients will use this method async Task<RootObject<T>> Request(string query) { return await this.myService.GetContentAndReadAsTypeAsync(await this.CreateRequestAsync(query)); } }
The API Service class is a messaging gateway, which encapsulates the API messaging code, and has strongly-typed methods. (See especially “public Task<RootObject> GetContentAndReadAsType(ApiRequest request)”.)
public abstract class ApiService<T> : IApiService<T> { private static FormUrlEncodedContent CreateFormUrlEncodedContentForGetAccessToken(string consumerKey, string consumerSecret) { //Data required by OSU Get Access Token API var keys = { new KeyValuePair<string, string>("grant_type", "client_credentials"), new KeyValuePair<string, string>("client_id", consumerKey), new KeyValuePair<string, string>("client_secret", consumerSecret) }; //Also sets content headers content type to "application/x-www-form-urlencoded". return new FormUrlEncodedContent(keys); } //"Async" declaration because we are calling "GetAsync" asynchronous method. //.NET compiler builds an absolutely CRAZY state machine in code behind the scenes. private async static Task<HttpResponseMessage> GetResponseAsync(HttpClient client, ApiRequest request) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", request.AccessToken); //Await-ing a result suspends its progress and yields control to the method that called it. //Could actually do other things in parallel here before we Await: // Dim responseTask = client.Get(request.ResourceRelativeUriText & request.Query) // DoIndependentWork() // Dim response = await responseTask var response = await client.Get(request.ResourceRelativeUriText + request.Query); //ApiService doesn't know how to handle failed responses yet, so throw exception. if (!response.IsSuccessStatusCode) { throw new InvalidOperationException(response.ReasonPhrase); } return response; } private static HttpClient CreateHttpClient(string baseAddressUriText) { return new HttpClient { BaseAddress = new Uri(baseAddressUriText) }; } public Task<string> GetAccessToken(string baseAddressUriText, string resourceRelativeUriText, string consumerKey, string consumerSecret) { //for a given resource Uri, like "v1/locations" or "v1/locations/", append segment for the access token to get "v1/locations/token" //todo: improve with something like http://stackoverflow.com/questions/372865/path-combine-for-urls var requestUri = new Uri(resourceRelativeUriText.TrimEnd('/') + "/token", UriKind.Relative); var content = CreateFormUrlEncodedContentForGetAccessToken(consumerKey, consumerSecret); //"Using" automatically and absolutely calls "Dispose" method on object at the code block. //Disposes any expensive and/or unmanaged resources, like an open connection stream. using (client = CreateHttpClient(baseAddressUriText)) { using (response = client.Post(requestUri, content)) { if (!response.IsSuccessStatusCode) { throw new InvalidOperationException(response.ReasonPhrase); } //Shorthand for: // var response = await response.Content.ReadAs(Of GetAccessTokenResponse)() // Return response.access_token return (response.Content.ReadAs<GetAccessTokenResponse>()).access_token; } } } //Read as string to support troubleshooting and curiosity. //Most clients should use the other method to "read as type," which reads data into strongly-typed classes public Task<string> GetContentAndReadAsString(ApiRequest request) { using (client = CreateHttpClient(request.BaseAddressUriText)) { using (response = GetResponseAsync(client, request)) { return response.Content.ReadAsString(); } } } public Task<RootObject<T>> GetContentAndReadAsType(ApiRequest request) { using (client = CreateHttpClient(request.BaseAddressUriText)) { using (response = GetResponseAsync(client, request)) { //The magic deserialization of JSON // into .NET classes happens in this one line... //Declaration of ReadAs is: // Public Shared Function ReadAs(Of T)(content As System.Net.Http.HttpContent) As System.Threading.Tasks.Task(Of T) //This class (ApiService) is generic so we can take advantage of ReadAs being generic. return response.Content.ReadAs<RootObject<T>>(); } } } }
And finally, the Location Service class inherits from API Service with the type parameter LocationData.
//Inherits ApiService, so we get its interface and functionality for free. public class LocationService : ApiService<LocationData> { }
(This WordPress blog did not seem to have syntax highlighting for Visual Basic. So, I translated my existing code from VB.NET to C# using several online translators. There are bound to be errors introduced in the translation process. I know, I know. It’s not “cool” to write in VB… the poor under-appreciated language that can do everything–and more!–its currently-hip sibling C# can do. But I have more than 17 years of VB development under my belt, so not switching now just to be cool. I prefer to deliver business value rather than struggle with silly braces and semicolons. 🙂
Thanks! We strive to making the APIs consistent so that developers have fewer lines of code to write before integrating with different data sources.
On a side note, I agree VB may not be as popular or hip as the C#, but it’s a good language. It was one of the first languages I learned back in college 🙂 .