65.9K
CodeProject 正在变化。 阅读更多。
Home

C# CodeProject API 包装器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.59/5 (8投票s)

2016年3月3日

CPOL

8分钟阅读

viewsIcon

64293

downloadIcon

1127

让我们看看如何构建一个简单的 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。

https://api.codeproject.com

向消费者公开了两种类型的 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。

https://api.codeproject.com

动手使用 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/ 中进行测试。

AccessToken

以下是我们发出的 Http GET 请求,用于获取我的文章,并将授权头设置为 "Bearer <space> <access token>"。

https://api.codeproject.com/v1/my/articles?page=1

MyArticle-Request

以下是相同的 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

MyArticle-Response

使用代码

让我们开始实现包装器以使用 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

© . All rights reserved.