Introduction
It all started when one of our beloved CodeProject member "John Simmons / outlaw programmer" posted the below question in Lounge.
http://www.codeproject.com/Lounge.aspx?msg=5006963#xx5006963xx
"Has anyone taken it upon themselves to come up with a c# wrapper around the CodeProject APIs?" I was in fact having a partial implementation of the so called API helpers. His question brought into my attention in building a useful wrapper.
In this article, let us take a look into how we can write a simple wrapper for accessing the CodeProject API.
First take a look into the CodeProject API
https://api.codeproject.com
There are two types of API’s which are exposed to the consumers.
My API – You can use this APIs for accessing the logged in CodeProject user information such as your articles, blogs, messages, questions etc. Every request must have the OAuth token which is being passed as a part of the HTTP Request header as "Authorization" with a value as "Bearer <token>".
Here’s an example to get all my bookmarks with a page size as 1.
https://api.codeproject.com/v1/my/bookmarks?page=1
Other API’s – It’s the generally available contents from CodeProject such as Articles, ForumMessages, and Questions etc. You can access these contents through the token which you have obtained or you can also access the same through registered ClientId and Client Secret.
The first thing you have to do is, register with an application name and obtain the client Id and Secret key which we will be using at a later point of time for MyAPI queries.
https://www.codeproject.com/script/webapi/userclientregistrations.aspx
We will be discussing the below topics.
Background
If you are new to CodeProject API, please take a look into the below link for more understanding on the API.
https://api.codeproject.com
Let us get our hands wet before we code the wrapper implementation. Navigate to the below URL for MyApi CodeProject API Sample.
https://api.codeproject.com/Samples/MyApi
The above URL lets you login with your CodeProject member ID and password. After the OAuth authentication, you should be able to see all your profile information.
Here’s a small trick to get the access token. If you are using Google Chrome, you can use inspect element, which will open up the Developer tools. Navigate to "Session Storage" and you will see the Key-Value pair with the key as "accessToken". Copy the value; we need this to test the same in https://www.hurl.it/
Below is the Http GET Request we are making to get my articles with the authorization header set with a value as Bearer <space> <access token>
https://api.codeproject.com/v1/my/articles?page=1
Below is the HTTP response for the same. If you have posted articles to CodeProject, you should see your articles getting listed.
Cache-Control: no-cache
Content-Length: 25291
Content-Type: application/json; charset=utf-8
Date: Sun, 22 Feb 2015 01:11:50 GMT
Expires: -1
Pragma: no-cache
Server: Microsoft-IIS/7.5
X-Aspnet-Version: 4.0.30319
X-Powered-By: ASP.NET
Using the code
Let us get started in implementing the Wrapper to consume the CodeProject API. We will be implementing the other API and My API functionalities and try to get some useful things.
We will start with the list of entities. Below is the CodeProjectRoot object, contains information related to pagination and items. The "items" holds the actual data.
public class CodeProjectRoot
{
public Pagination pagination { get; set; }
public List<Item> items { get; set; }
}
Below is the code snippet of "Item" class. You can see below, it holds all the related entities such as Author, Category, Tag, License information etc.
public class Item
{
public string id { get; set; }
public string title { get; set; }
public List<Author> authors { get; set; }
public string summary { get; set; }
public string contentType { get; set; }
public DocType docType { get; set; }
public List<Category> categories { get; set; }
public List<Tag> tags { get; set; }
public License license { get; set; }
public string createdDate { get; set; }
public string modifiedDate { get; set; }
public ThreadEditor threadEditor { get; set; }
public string threadModifiedDate { get; set; }
public double rating { get; set; }
public int votes { get; set; }
public double popularity { get; set; }
public string websiteLink { get; set; }
public string apiLink { get; set; }
public int parentId { get; set; }
public int threadId { get; set; }
public int indentLevel { get; set; }
}
Most of the result set which is returned by the CodeProject API is paged result. Below is the code snippet of pagination class, holds page and item related information.
public class Pagination
{
public int page { get; set; }
public int pageSize { get; set; }
public int totalPages { get; set; }
public int totalItems { get; set; }
}
Below is the code snippet of Author entity holds the author name and Id.
public class Author
{
public string name { get; set; }
public int id { get; set; }
}
Every request to CodeProject API must have an Authorization header with the AccessToken. The access token is nothing but a unique OAuth token returned based on the Client Id and Client Secret Keys.
Get Access Token
Let us build a small helper class named "CodeProjectAccessToken" to get the access token so that we can make use of the same at a later point of time when we are actually making a request to get questions, answers etc. Below is the reused code from CodeProject API Sample.
public async Task<string> GetAccessToken()
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(_baseUrl);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
List<KeyValuePair<string, string>> postData = new List<KeyValuePair<string, string>>();
postData.Add(new KeyValuePair<string, string>("grant_type", "client_credentials"));
postData.Add(new KeyValuePair<string, string>("client_id", _clientId));
postData.Add(new KeyValuePair<string, string>("client_secret", _clientSecret));
FormUrlEncodedContent content = new FormUrlEncodedContent(postData);
HttpResponseMessage response = await client.PostAsync("Token", content);
string jsonString = await response.Content.ReadAsStringAsync();
object responseData = JsonConvert.DeserializeObject(jsonString);
return ((dynamic)responseData).access_token;
}
}
Fetch CodeProject Questions
Now we will be having a look into the code snippet to all the questions based on the tags such as C#, ASP.NET etc. Question mode (Default, Unanswered, Active, New), created data and modified date.
public async Task<List<Item>> GetCodeProjectQuestions(string tags, QuestionListMode mode,
DateTime createdDate, DateTime modifiedDate)
{
_allItems.Clear();
return await GetQuestions(tags, mode, createdDate, modifiedDate);
}
You can see below the private method called with all the required parameters so that it returns the list of "Item" collection by recursively making a call and trying to include the item if it matches the created and modified date criteria if you specify one.
private async Task<List<Item>> GetQuestions(string tags, QuestionListMode mode,
DateTime createdDate, DateTime modifiedDate,
int pageCount = 1)
{
var questionRoot = await codeProjectApiHelper.GetQuestions(pageCount, mode, tags);
if (questionRoot.items == null)
return _allItems;
foreach (var question in questionRoot.items)
{
var questionModifiedDate = DateTime.Parse(question.modifiedDate);
if (questionModifiedDate.Date != modifiedDate.Date && modifiedDate != DateTime.MinValue)
{
if (questionModifiedDate.Date > modifiedDate.Date)
continue;
if (questionModifiedDate.Date < modifiedDate.Date)
return _allItems;
}
var questionCreatedDate = DateTime.Parse(question.createdDate);
if (questionCreatedDate.Date != createdDate.Date && createdDate != DateTime.MinValue)
{
if (questionCreatedDate.Date > createdDate.Date)
continue;
if (questionCreatedDate.Date < createdDate.Date)
return _allItems;
}
_allItems.Add(question);
}
if (pageCount > questionRoot.pagination.totalPages) return _allItems;
pageCount++;
await GetQuestions(tags, mode, createdDate, modifiedDate, pageCount);
return _allItems;
}
In the above code, you can see the actual call to get the questions is being implemented in "CodeProjectAPIHelper" class. Below is the code snippet for the same.
public async Task<CodeProjectRoot> GetQuestions(int page, QuestionListMode mode, string tags)
{
using (var client = new HttpClient())
{
SetHttpClientProperties(client);
var url = string.Format("v1/Questions/{0}?page={1}&include={2}", mode, page, HttpUtility.UrlEncode(tags));
var response = await client.GetAsync(url);
var jsonResponseString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CodeProjectRoot>(jsonResponseString);
}
}
You can see a method call to set the HttpClient properties. There are couple of things such as base address, accept type and authorization needs to be set. Here’s the code snippet for the same.
private void SetHttpClientProperties(HttpClient client)
{
client.BaseAddress = new Uri(_baseUrl);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + _accessToken);
}
Fetch CodeProject Articles
Now let us see how to fetch CodeProject Articles. Below is the code snippet, shows how to make use of the wrapper class.
var codeProjectApiWrapper = new CodeProjectApiWrapper(baseUrl, _accessToken);
var articles = await codeProjectApiWrapper.GetMyArticles();
foreach (var article in articles)
{
Console.WriteLine("Title: {0}", article.title);
foreach (var author in article.authors)
{
Console.WriteLine("Authors: {0}", author.name);
}
}
Below is the code snippet for fetching the articles. It’s in CodeProjectApiWrapper class. You will notice the code looks similar to the one which we did in getting all questions.
public async Task<List<Item>> GetCodeProjectArticles(string tags,
DateTime createdDate,
DateTime modifiedDate,
double? minRating = null,
double? minRatingGreaterThanEqual = null)
{
_allItems.Clear();
return await GetArticles(tags, createdDate, modifiedDate, minRating, minRatingGreaterThanEqual);
}
private async Task<List<Item>> GetArticles(string tags,
DateTime createdDate,
DateTime modifiedDate,
double? minRating = null,
double? minRatingGreaterThanEqual = null,
int pageCount = 1)
{
var articleRoot = await codeProjectApiHelper.GetArticles(pageCount, tags);
if (articleRoot.items == null)
return _allItems;
foreach (var article in articleRoot.items)
{
var articleModifiedDate = DateTime.Parse(article.modifiedDate);
if (articleModifiedDate.Date != modifiedDate.Date && modifiedDate != DateTime.MinValue)
{
if (articleModifiedDate.Date > modifiedDate.Date)
continue;
if (articleModifiedDate.Date < modifiedDate.Date)
return _allItems;
}
var articleCreatedDate = DateTime.Parse(article.createdDate);
if (articleCreatedDate.Date != modifiedDate.Date && createdDate != DateTime.MinValue)
{
if (articleCreatedDate.Date > createdDate.Date)
continue;
if (articleCreatedDate.Date < createdDate.Date)
return _allItems;
}
if (minRating == null || article.rating != minRating)
{
if (minRatingGreaterThanEqual == null)
{
_allItems.Add(article);
}
else
{
if (article.rating >= minRatingGreaterThanEqual)
_allItems.Add(article);
}
}
else
_allItems.Add(article);
}
if (pageCount > articleRoot.pagination.totalPages) return _allItems;
pageCount++;
await GetArticles(tags, modifiedDate, createdDate, minRating, minRatingGreaterThanEqual, pageCount);
return _allItems;
}
Fetch Forum Messages
Let us see how we can fetch the forum messages. Note – You will have to specify the forum Id and forum display mode (Messages or Threads).
Below you see, we are passing a hardcoded forum Id and forum display mode as ‘Messages’
var createdDate = DateTime.Parse("2012-07-02");
Console.WriteLine(string.Format("------ Forum Messages for {0}------", createdDate.ToShortDateString()));
var forumMessages = await codeProjectApiWrapper.GetCodeProjectForumMessages(1159, ForumDisplayMode.Messages,
createdDate, DateTime.MinValue);
foreach (var forumMessage in forumMessages)
{
Console.WriteLine("Title: {0}", forumMessage.title);
Console.WriteLine("Date: {0}", forumMessage.createdDate);
}
The code within the Wrapper class is as below.
public async Task<List<Item>> GetCodeProjectForumMessages(int forumId,
ForumDisplayMode mode,
DateTime createdDate,
DateTime modifiedDate)
{
_allItems.Clear();
return await GetForumMessages(forumId, mode, createdDate, modifiedDate);
}
private async Task<List<Item>> GetForumMessages(int forumId,
ForumDisplayMode mode,
DateTime createdDate,
DateTime modifiedDate,
int pageCount = 1)
{
var forumMessageRoot = await codeProjectApiHelper.GetForumMessages(forumId, mode, pageCount);
if (forumMessageRoot.items == null)
return _allItems;
foreach (var forumMessage in forumMessageRoot.items)
{
var forumModifiedDate = DateTime.Parse(forumMessage.modifiedDate);
if (forumModifiedDate.Date != modifiedDate.Date && modifiedDate != DateTime.MinValue)
{
if (forumModifiedDate.Date > modifiedDate.Date)
continue;
if (forumModifiedDate.Date < modifiedDate.Date)
return _allItems;
}
var forumMessageCreatedDate = DateTime.Parse(forumMessage.createdDate);
if (forumMessageCreatedDate.Date != createdDate.Date && createdDate != DateTime.MinValue)
{
if (forumMessageCreatedDate.Date > createdDate.Date)
continue;
if (forumMessageCreatedDate.Date < createdDate.Date)
return _allItems;
}
_allItems.Add(forumMessage);
}
if (pageCount > forumMessageRoot.pagination.totalPages) return _allItems;
pageCount++;
await GetForumMessages(forumId, mode, createdDate, modifiedDate, pageCount);
return _allItems;
}
My API
The My API’s are those API’s that provides access to CodeProject members information such as profile, reputations, questions, answers, articles, blogs, tips etc. Now let us take a look into the usage of My API with a specific code samples for the same. The My API response is obtained by making use of a specific piece of reused code.
Below is the code snippet shows the usage on how to get all my articles.
var codeProjectApiWrapper = new CodeProjectApiWrapper(baseUrl, _accessToken);
var articles = await codeProjectApiWrapper.GetMyArticles();
foreach (var article in articles)
{
Console.WriteLine("Title: {0}", article.title);
foreach (var author in article.authors)
{
Console.WriteLine("Authors: {0}", author.name);
}
}
Here’s how it internally works. The GetMyArticles method is called with a default page count as one. Further it makes a call to CodeProjectApiHelper method GetMyContent which makes a HTTP GET request to get the article contents.
public async Task<List<Item>> GetMyArticles(int pageCount = 1)
{
_allItems.Clear();
return await GetMyContent(pageCount, MyAPIEnum.Articles);
}
private async Task<List<Item>> GetMyContent(int pageCount, MyAPIEnum myAPIEnum)
{
var rootObject = await codeProjectApiHelper.GetMyContent(pageCount, myAPIEnum);
if (rootObject.items == null)
return _allItems;
foreach (var item in rootObject.items)
{
_allItems.Add(item);
}
if (pageCount > rootObject.pagination.totalPages) return _allItems;
pageCount++;
await GetMyContent(pageCount, myAPIEnum);
return _allItems;
}
Below is the code snippet of CodeProjectApiHelper method GetMyContent. You can also see below how we are building the URL based on the MyAPIEnum.
Note – The HTTP response for Notification, Profile and Reputation are totally different and we will see next how we are dealing with those.
#region "My API"
public string GetMyAPIUrl(int page, MyAPIEnum myAPIEnum)
{
switch (myAPIEnum)
{
case MyAPIEnum.Notifications:
return "v1/my/notifications";
break;
case MyAPIEnum.Profile:
return "v1/my/profile";
break;
case MyAPIEnum.Reputation:
return "v1/my/reputation";
break;
}
return string.Format("v1/my/{0}?page={1}", myAPIEnum.ToString().ToLower(), page);
}
public async Task<CodeProjectRoot> GetMyContent(int page, MyAPIEnum myAPIEnum)
{
using (var client = new HttpClient())
{
SetHttpClientProperties(client);
var response = await client.GetAsync(GetMyAPIUrl(page, myAPIEnum));
var jsonResponseString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<CodeProjectRoot>(jsonResponseString);
}
}
#endregion
My Questions, Answers, Tips, Blogs, Bookmarks all work in a similar way as fetching of My Articles works.
The code for fetching the notifications, profile and reputation information for the My API is all done by using a specific set of classes for de-serializing the HTTP Response.
We need to have a specific set of entities for desterilizing the JSON responses for notification. Below is the code snippet for the notification entities.
public class Notification
{
public int id { get; set; }
public string objectTypeName { get; set; }
public int objectId { get; set; }
public string subject { get; set; }
public string topic { get; set; }
public string notificationDate { get; set; }
public bool unRead { get; set; }
public string content { get; set; }
public string link { get; set; }
}
public class NotificationRoot
{
public List<Notification> notifications { get; set; }
}
Below is the code snippet where you can see how we are making a HTTP GET Request to the URL that is built by reusing the code GetMyAPIUrl, notice below we are passing "-1" for page count as it’s not really being use for notifications.
Once we get the JSON response, we will be desterilizing the same by using NotificationRoot class.
public async Task<NotificationRoot> GetMyNotifications()
{
using (var client = new HttpClient())
{
codeProjectApiHelper.SetHttpClientProperties(client);
var response = await client.GetAsync(codeProjectApiHelper.GetMyAPIUrl(-1, MyAPIEnum.Notifications));
var jsonResponseString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<NotificationRoot>(jsonResponseString);
}
}
My Reputation
Let us see how to fetch the reputation points. Below is the code snippet where we do a console print for the Total reputation points.
Console.WriteLine("Get all my Reputation");
var reputation = await codeProjectApiWrapper.GetMyReputation();
Console.WriteLine("Total Points: "+reputation.totalPoints);
Here are the set to entities required for de-serializing the reputation JSON response.
public class ReputationType
{
public string name { get; set; }
public int points { get; set; }
public string level { get; set; }
public string designation { get; set; }
}
public class Reputation
{
public int totalPoints { get; set; }
public List<ReputationType> reputationTypes { get; set; }
public string graphUrl { get; set; }
}
The code for making the HTTP GET Request for getting the reputation points looks almost similar to that of notifications. You can see below, how we are de-serializing the JSON response by utilizing the Reputation entity.
public async Task<Reputation> GetMyReputation()
{
using (var client = new HttpClient())
{
codeProjectApiHelper.SetHttpClientProperties(client);
var response = await client.GetAsync(codeProjectApiHelper.GetMyAPIUrl(-1, MyAPIEnum.Reputation));
var jsonResponseString = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Reputation>(jsonResponseString);
}
}
My Profile
At last, let us see how to fetch the profile information. Below is the code snippet for the same.
Console.WriteLine("Get all my Profile");
var profileInfo = await codeProjectApiWrapper.GetMyProfile();
Console.WriteLine("Display Name: " + profileInfo.displayName);
Console.WriteLine("Company: " + profileInfo.company);
Here’s the Profile entity that we will be using it to de-serialize the JSON response for profile.
public class Profile
{
public int id { get; set; }
public string userName { get; set; }
public string displayName { get; set; }
public string avatar { get; set; }
public string email { get; set; }
public bool htmlEmails { get; set; }
public string country { get; set; }
public string homePage { get; set; }
public int codeProjectMemberId { get; set; }
public string memberProfilePageUrl { get; set; }
public string twitterName { get; set; }
public string googlePlusProfile { get; set; }
public string linkedInProfile { get; set; }
public string biography { get; set; }
public string company { get; set; }
public string jobTitle { get; set; }
}
Points of Interest
There’s always something new I’m learning and this time, I was really happy about building a generic extendable wrapper for CodeProject API that one can use it in no time without having to understand much about the internals. Although it’s a simply HTTP GET Requests and we de-serialize the response to appropriate type, few instance where I felt like there could be some improvements in the API for example, the question that I have posted below for considering the created date and modified date as filter parameters while making the request would greatly help a lot of looping that we are doing within the code that you might have already noticed and wondered is it at required? I would say, yes at this point. But I am sure there will be more improvements based on our feedback.
http://www.codeproject.com/Messages/5000498/Regarding-GetArticles.aspx
Currently there is one TODO item – handling unauthorized access exception(occurs when you are using a invalid credentials or access token) and letting the user know about the this exception.
References
Please take a look into https://api.codeproject.com to understand more about CodeProject API.
History
Version 1.0 - Coding the API Wrapper with the code smaple to fetch CodeProject and My API related info - 02/22/2015.
Version 1.1 - Updated solution, created a portable library and referenced the same in console app - 02/22/2015