C# CodeProject API 包装器
让我们看看如何构建一个简单的 CodeProject API 包装器。
引言
一切都始于我们一位亲爱的 CodeProject 会员 "John Simmons / outlaw programmer" 在 Lounge 发布的以下问题。
https://codeproject.org.cn/Lounge.aspx?msg=5006963#xx5006963xx
"有人自己动手为 CodeProject API 创建了一个 C# 包装器吗?" 事实上,我当时已经有了一个部分实现的所谓 API 助手。他的问题引起了我对构建一个有用包装器的注意。
在本文中,让我们来看看如何编写一个简单的包装器来访问 CodeProject API。
首先,让我们了解一下 CodeProject API。
向消费者公开了两种类型的 API。
我的 API – 您可以使用这些 API 来访问登录的 CodeProject 用户信息,例如您的文章、博客、消息、问题等。每个请求都必须包含 OAuth 令牌,该令牌作为 HTTP 请求头的一部分,以 "Authorization" 的形式传递,值为 "Bearer <token>"。
以下是一个以 1 页大小获取我所有书签的示例。
https://api.codeproject.com/v1/my/bookmarks?page=1
其他 API – 这是 CodeProject 上普遍可用的内容,例如文章、论坛消息和问题等。您可以通过您已获得的令牌来访问这些内容,也可以通过注册的 ClientId 和 Client Secret 来访问它们。
您首先需要做的是,注册一个应用程序名称并获取我们稍后将用于 MyAPI 查询的客户端 ID 和密钥。
https://codeproject.org.cn/script/webapi/userclientregistrations.aspx
我们将讨论以下主题。
背景
如果您是 CodeProject API 的新手,请参阅以下链接以进一步了解 API。
动手使用 CodeProject API
在我们编写包装器实现代码之前,先动手试试。导航到以下 URL 以查看 MyApi CodeProject API 示例。
https://api.codeproject.com/Samples/MyApi
上述 URL 允许您使用您的 CodeProject 会员 ID 和密码登录。经过 OAuth 身份验证后,您应该能够看到您的所有个人资料信息。
以下是一个获取访问令牌的小技巧。如果您使用的是 Google Chrome,可以使用“检查元素”打开开发者工具。导航到“Session Storage”,您会看到一个键值对,键为 "accessToken"。复制该值;我们需要它来在 https://www.hurl.it/ 中进行测试。
以下是我们发出的 Http GET 请求,用于获取我的文章,并将授权头设置为 "Bearer <space> <access token>"。
https://api.codeproject.com/v1/my/articles?page=1
以下是相同的 HTTP 响应。如果您发布过文章到 CodeProject,您应该会看到您的文章列表。
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
使用代码
让我们开始实现包装器以使用 CodeProject API。我们将实现其他 API 和我的 API 功能,并尝试获取一些有用的信息。
我们将从实体列表开始。以下是 CodeProjectRoot 对象,它包含与分页和项目相关的信息。"items" 包含实际数据。
public class CodeProjectRoot { public Pagination pagination { get; set; } public List<Item> items { get; set; } }
以下是 "Item" 类的代码片段。您可以在下方看到,它包含了所有相关的实体,例如作者、类别、标签、许可证信息等。
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; } }
CodeProject API 返回的大部分结果集都是分页结果。以下是分页类的代码片段,它包含页面和项目相关的信息。
public class Pagination { public int page { get; set; } public int pageSize { get; set; } public int totalPages { get; set; } public int totalItems { get; set; } }
以下是 Author 实体的代码片段,它包含作者姓名和 ID。
public class Author { public string name { get; set; } public int id { get; set; } }
每个请求 CodeProject API 都必须有一个带有 AccessToken 的 Authorization 头。访问令牌就是基于客户端 ID 和客户端密钥返回的唯一 OAuth 令牌。
获取访问令牌
让我们构建一个名为 "CodeProjectAccessToken" 的小型帮助类来获取访问令牌,以便我们以后在实际发出请求以获取问题、答案等时使用。以下是 CodeProject API 示例中重用的代码。
public async Task<string> GetAccessToken() { using (var client = new HttpClient()) { client.BaseAddress = new Uri(_baseUrl); // We want the response to be JSON. client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // Build up the data to POST. 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); // Post to the Server and parse the response. HttpResponseMessage response = await client.PostAsync("Token", content); string jsonString = await response.Content.ReadAsStringAsync(); object responseData = JsonConvert.DeserializeObject(jsonString); // return the Access Token. return ((dynamic)responseData).access_token; } }
获取 CodeProject 问题
现在我们将查看代码片段,以根据 C#、ASP.NET 等标签获取所有问题。问题模式(默认、未回答、活动、新)、创建日期和修改日期。
public async Task<List<Item>> GetCodeProjectQuestions(string tags, QuestionListMode mode, DateTime createdDate, DateTime modifiedDate) { _allItems.Clear(); return await GetQuestions(tags, mode, createdDate, modifiedDate); }
您可以在上面的代码中看到,调用私有方法时传入了所有必需的参数,以便它通过递归调用来返回 "Item" 集合列表,并尝试在指定了创建日期和修改日期标准的情况下包含匹配的项。
/// <summary> /// Gets the page of Questions. /// </summary> /// <param name="tags">Tags</param> /// <param name="createdDate">Created Date</param> /// <param name="modifiedDate">Modified Date</param> /// <param name="pageCount">Page Count</param> /// <returns>List of Items</returns> 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; }
在上面的代码中,您可以看到实际获取问题的调用是在 "CodeProjectAPIHelper" 类中实现的。以下是代码片段。
/// <summary> /// Gets the page of Questions. /// </summary> /// <param name="page">The page to get.</param> /// <param name="tags">The tags to filter the articles with.</param> /// <returns>The page of articles.</returns> public async Task<CodeProjectRoot> GetQuestions(int page, QuestionListMode mode, string tags) { using (var client = new HttpClient()) { SetHttpClientProperties(client); // create the URL string. var url = string.Format("v1/Questions/{0}?page={1}&include={2}", mode, page, HttpUtility.UrlEncode(tags)); // make the request var response = await client.GetAsync(url); // parse the response and return the data. var jsonResponseString = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<CodeProjectRoot>(jsonResponseString); } }
您可以看到调用用于设置 HttpClient 属性。有几件事情需要设置,例如基本地址、接受类型和授权。以下是代码片段。
private void SetHttpClientProperties(HttpClient client) { client.BaseAddress = new Uri(_baseUrl); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // Add the Authorization header with the AccessToken. client.DefaultRequestHeaders.Add("Authorization", "Bearer " + _accessToken); }
获取 CodeProject 文章
现在让我们看看如何获取 CodeProject 文章。以下是代码片段,展示了如何使用包装器类。
// Create an instance of CodeProjectApiWrapper // It has all the juicy methods, helps in accessing the CodeProject API's var codeProjectApiWrapper = new CodeProjectApiWrapper(baseUrl, _accessToken); var articles = await codeProjectApiWrapper.GetMyArticles(); // My Articles foreach (var article in articles) { Console.WriteLine("Title: {0}", article.title); foreach (var author in article.authors) { Console.WriteLine("Authors: {0}", author.name); } }
以下是获取文章的代码片段。它位于 CodeProjectApiWrapper 类中。您会注意到代码与我们获取所有问题的方式非常相似。
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); } /// <summary> /// Gets the page of Articles. /// </summary> /// <param name="tags">Tags</param> /// <param name="createdDate">Created Date</param> /// <param name="modifiedDate">Modified Date</param> /// <param name="minRating">Minimum Rating</param> /// <param name="minRatingGreaterThanEqual">Minimum rating greater than or equal to</param> /// <param name="pageCount">Page Count, Default = 1</param> /// <returns>List of Items</returns> 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; }
获取论坛消息
让我们看看如何获取论坛消息。注意 – 您需要指定论坛 ID 和论坛显示模式(消息或主题)。
下面我们看到,我们正在传递一个硬编码的论坛 ID 和论坛显示模式为 '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); }
Wrapper 类中的代码如下。
public async Task<List<Item>> GetCodeProjectForumMessages(int forumId, ForumDisplayMode mode, DateTime createdDate, DateTime modifiedDate) { _allItems.Clear(); return await GetForumMessages(forumId, mode, createdDate, modifiedDate); } /// <summary> /// Returns all Forum Messages for the Forum ID, Page Count and Created or Modified Date /// </summary> /// <param name="forumId">Forum ID</param> /// <param name="createdDate">Created Date</param> /// <param name="modifiedDate">Modified Date</param> /// <param name="pageCount">Page Count</param> 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; }
我的 API
我的 API 是那些提供对 CodeProject 会员信息(如个人资料、声誉、问题、答案、文章、博客、技巧等)的访问的 API。现在让我们通过一些特定的代码示例来看看我的 API 的用法。我的 API 响应是通过使用一段重用的代码获得的。
以下是显示如何获取我所有文章用法的代码片段。
var codeProjectApiWrapper = new CodeProjectApiWrapper(baseUrl, _accessToken); var articles = await codeProjectApiWrapper.GetMyArticles(); // My Articles foreach (var article in articles) { Console.WriteLine("Title: {0}", article.title); foreach (var author in article.authors) { Console.WriteLine("Authors: {0}", author.name); } }
它是如何内部工作的。GetMyArticles 方法以默认一页的数量被调用。然后它调用 CodeProjectApiHelper 方法 GetMyContent,该方法发出 HTTP GET 请求以获取文章内容。
/// <summary> /// Returns my articles /// </summary> /// <param name="pageCount">Page Count</param> /// <returns>List of Item</returns> public async Task<List<Item>> GetMyArticles(int pageCount = 1) { _allItems.Clear(); return await GetMyContent(pageCount, MyAPIEnum.Articles); } /// <summary> /// A generic function returns content based on the page count and My API Enum /// </summary> /// <param name="pageCount">Page Count</param> /// <param name="myAPIEnum">My API Enum</param> /// <returns>List of Item</returns> 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; }
以下是 CodeProjectApiHelper 方法 GetMyContent 的代码片段。您还可以看到我们如何根据 MyAPIEnum 构建 URL。
注意 – Notification、Profile 和 Reputation 的 HTTP 响应完全不同,我们接下来将看到如何处理它们。
#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); } /// <summary> /// Returns the response based on the My API Type and page number in most of the cases /// </summary> /// <param name="page">Page Number</param> /// <param name="myAPIEnum">My API Enum</param> /// <returns>CodeProjectRoot instance</returns> public async Task<CodeProjectRoot> GetMyContent(int page, MyAPIEnum myAPIEnum) { using (var client = new HttpClient()) { SetHttpClientProperties(client); // make the request var response = await client.GetAsync(GetMyAPIUrl(page, myAPIEnum)); // parse the response and return the data. var jsonResponseString = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<CodeProjectRoot>(jsonResponseString); } } #endregion
我的问题、答案、技巧、博客、书签都与获取我的文章的工作方式类似。
用于获取我的 API 的通知、个人资料和声誉信息的代码是通过使用一组特定的类来反序列化 HTTP 响应来完成的。
我的通知
我们需要一套特定的实体来反序列化通知的 JSON 响应。以下是通知实体的代码片段。
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; } }
以下是代码片段,您可以在其中看到我们如何向通过重用 GetMyAPIUrl 代码构建的 URL 发出 HTTP GET 请求,请注意我们传递了 "-1" 作为页数,因为它实际上并未用于通知。
一旦我们获得 JSON 响应,我们将使用 NotificationRoot 类来反序列化它。
public async Task<NotificationRoot> GetMyNotifications() { using (var client = new HttpClient()) { codeProjectApiHelper.SetHttpClientProperties(client); // make the request // we don't have a page count for this. Just pass a dummy value var response = await client.GetAsync(codeProjectApiHelper.GetMyAPIUrl(-1, MyAPIEnum.Notifications)); // parse the response and return the data. var jsonResponseString = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<NotificationRoot>(jsonResponseString); } }
我的声誉
让我们看看如何获取声誉积分。以下是代码片段,我们在其中控制台打印总声誉积分。
// My Reputation 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; } }
用于获取声誉积分的 HTTP GET 请求代码与通知的代码几乎相似。您可以在下面看到,我们是如何利用 Reputation 实体来反序列化 JSON 响应的。
/// <summary> /// Returns my reputation /// </summary> /// <returns>Returns a Reputation instance</returns> public async Task<Reputation> GetMyReputation() { using (var client = new HttpClient()) { codeProjectApiHelper.SetHttpClientProperties(client); // make the request // we don't have a page count for this. Just pass a dummy value var response = await client.GetAsync(codeProjectApiHelper.GetMyAPIUrl(-1, MyAPIEnum.Reputation)); // parse the response and return the data. var jsonResponseString = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<Reputation>(jsonResponseString); } }
我的个人资料
最后,让我们看看如何获取个人资料信息。以下是代码片段。
// My Profile Console.WriteLine("Get all my Profile"); var profileInfo = await codeProjectApiWrapper.GetMyProfile(); Console.WriteLine("Display Name: " + profileInfo.displayName); Console.WriteLine("Company: " + profileInfo.company);
这是我们将用于反序列化个人资料 JSON 响应的 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; } }
值得关注的点
我总是在学习新东西,这次,我很高兴能够为一个通用的、可扩展的 CodeProject API 包装器,人们可以立即使用它,而无需过多了解其内部机制。虽然这只是简单的 HTTP GET 请求,然后我们将响应反序列化为适当的类型,但有几个地方我觉得 API 可以改进,例如,我下面提出的关于将创建日期和修改日期作为过滤参数的请求问题,将极大地帮助我们减少代码中的循环,您可能已经注意到并疑惑是否需要。我想说,目前是需要的。但我相信基于我们的反馈,还会有更多改进。
https://codeproject.org.cn/Messages/5000498/Regarding-GetArticles.aspx
目前有一个待办事项 – 处理未经授权的访问异常(在使用无效凭据或访问令牌时发生),并告知用户此异常。
参考文献
请参阅 https://api.codeproject.com 以进一步了解 CodeProject API。
历史
版本 1.0 - 编写 API 包装器,并附带获取 CodeProject 和我的 API 相关信息的代码示例 - 2015/02/22。
版本 1.1 - 更新解决方案,创建了一个可移植库并将其引用到控制台应用程序 - 2015/02/22