使用N层架构风格从ASP.NET调用Stack Exchange REST API





1.00/5 (2投票s)
一个使用C#的ASP.NET应用程序,它调用Stack Exchange API并根据某些标准列出Stack Overflow问题。
引言
通过自定义开发的Web应用程序实现Stack Exchange(Stackoverflow)API https://api.stackexchange.com 的文档并不常见。因此,这是一个通过一个精简的客户端Web应用程序,ASP.NET Web应用程序来使用它的好机会。
我将创建一个完整的C# ASP.NET解决方案,该方案与Stack Exchange API进行交互,并根据某些标准列出Stack Overflow问题。Stack Exchange 是一个问答网站,用于讨论Stack Exchange系列问答网站。Stack Exchange API 是一个REST服务,它接受基于HTTP/HTTPS的URI查询字符串请求,并返回JSON/JSONP响应,使用户能够从网站上检索问题、答案、评论、徽章、事件、修订、建议编辑、用户信息和标签。
所有响应都经过压缩,包括GZIP或DEFLATE。如果您已将客户端应用程序注册到Stack Exchange网站,Stack Exchange API会实现OAuth 2.0进行身份验证。可以使用min、max、fromdate、todate和sort参数来对实时的Stack Exchange网站构成相当复杂的查询。
应用设计原则
本教程在实现Stack Exchange REST服务时采用了N层架构风格。同时,我们也可以在通过数据访问层处理数据库时应用相同的方法。你们都熟悉N层架构风格。但是,你们知道如何在代码中以恰当的方式实现它吗?本教程也回答了这个问题。
下图显示了我们将要实现的架构的构建块。那么,让我们从组件到组件开始演示。我们在这里从模型组件开始;正如你可能知道的,模型只是一个具有映射到数据库表及其列的属性的类。
下图显示了模型组件被分离为一个通用的类库,该类库将与N层架构的三层主要层、UI表示层、业务逻辑层和数据访问层进行交互。这将简化在任何这些层内处理实体模型。还有其他通用的组件,帮助组件,它们为我们的应用程序提供配置、日志记录、异常处理和其他功能。如果我们要以专业的方式构建解决方案,这些组件是必不可少的。
通常在我们的解决方案需要处理许多业务约束的情况下,例如ERP系统中的薪资约束,我们需要业务逻辑层,但在这里不是这种情况。因此,我们将忽略业务层的用法。我们将把它从我们的解决方案中省略。因此,我们将从数据访问层中的存储库类来处理和使用外部Stack Exchange API。
现在,让我们构建我们的解决方案。我使用了VS 2013和FW 4.5.2来构建解决方案,以下步骤是我在构建此演示应用程序时使用的顺序。
创建UI表示层
步骤(1):创建一个ASP.NET Web Forms项目并命名为StackClient
步骤(2):打开Web.Config文件并添加如下图所示的应用程序设置
<appSettings> <add key="StackClient.StackApiVersion" value="2.2" /> <add key="StackClient.StackApiUseHttps" value="True" /> <add key="StackClient.StackApiAccessKey" value="" /> <add key="StackClient.EnableLogging" value="True" /> <add key="StackClient.LoggingPath" value="D:\LoggingData" /> <add key="ValidationSettings:UnobtrusiveValidationMode" value="WebForms" /> </appSettings>
StackApiVersion
存储了Stack Exchange REST服务当前的版本,即2.2。StackApiUseHttps
标识是否使用HTTP或HTTPS协议。EnableLogging
设置为TRUE,以便在测试时启用日志记录,依此类推。
步骤(3):通过安装最新的NuGet包来更新你的jQuery库
步骤(4):安装jQuery-ui NuGet
步骤(5):将jQuery和jQuery-ui版本添加到Site.master页面的head部分,如下所示。
<head runat="server"> … <link href="~/Content/themes/base/jquery-ui.css" rel="stylesheet" /> </head>
并在Site.master页面的form主体内
<asp:ScriptManager runat="server"> <Scripts> <%--To learn more about bundling scripts in ScriptManager see http://go.microsoft.com/fwlink/?LinkID=301884 --%> <%--Framework Scripts--%> <asp:ScriptReference Name="MsAjaxBundle" /> <asp:ScriptReference Name="jquery" Path="~/Scripts/jquery-3.1.1.min.js"/> <asp:ScriptReference Path="~/Scripts/jquery-ui-1.12.1.min.js" /> <asp:ScriptReference Name="bootstrap" /> <asp:ScriptReference Name="respond" /> <asp:ScriptReference Name="WebForms.js" Assembly="System.Web" Path="~/Scripts/WebForms/WebForms.js" /> <asp:ScriptReference Name="WebUIValidation.js" Assembly="System.Web" Path="~/Scripts/WebForms/WebUIValidation.js" /> <asp:ScriptReference Name="MenuStandards.js" Assembly="System.Web" Path="~/Scripts/WebForms/MenuStandards.js" /> <asp:ScriptReference Name="GridView.js" Assembly="System.Web" Path="~/Scripts/WebForms/GridView.js" /> <asp:ScriptReference Name="DetailsView.js" Assembly="System.Web" Path="~/Scripts/WebForms/DetailsView.js" /> <asp:ScriptReference Name="TreeView.js" Assembly="System.Web" Path="~/Scripts/WebForms/TreeView.js" /> <asp:ScriptReference Name="WebParts.js" Assembly="System.Web" Path="~/Scripts/WebForms/WebParts.js" /> <asp:ScriptReference Name="Focus.js" Assembly="System.Web" Path="~/Scripts/WebForms/Focus.js" /> <asp:ScriptReference Name="WebFormsBundle" /> <%--Site Scripts--%> </Scripts> </asp:ScriptManager>
创建通用的辅助组件
步骤(6):创建一个名为“StackClient.StackExchange.Common”的类库。它将代表通用的辅助组件。我们创建“ConfigurationHandler”、“ExceptionHandler”、“LoggingHandler”、“FileHandler”和“EnumHandler”C#文件。请参考附带的解决方案源代码来找到它们。
这里需要提到的一点是,ConfigurationHandler
类将在实例化时从web.config文件获取配置设置,如下面的C#代码所示。
private void InitConfigurationHandler()
{
StackApiAccessKey = GetAppSettingsValueByKey("StackClient.StackApiAccessKey");
StackApiVersion = GetAppSettingsValueByKey("StackClient.StackApiVersion");
StackApiUseHttps = bool.Parse(GetAppSettingsValueByKey("StackClient.StackApiUseHttps").ToLower());
StackApiUrl = StackApiApiBaseUrl.Replace("{Protocol}", StackApiUseHttps ? "https" : "http").Replace("{Version}", StackApiVersion);
}
/// <summary>
/// Purpose: ConfigurationHandler class constructor
/// </summary>
public ConfigurationHandler()
{
InitConfigurationHandler();
}
public string GetAppSettingsValueByKey(string sKey)
{
try
{
if (string.IsNullOrEmpty(sKey))
throw new ArgumentNullException("sKey", "The AppSettings key name can't be Null or Empty.");
if (ConfigurationManager.AppSettings[sKey] == null)
throw new ConfigurationErrorsException(string.Format("Failed to find the AppSettings Key named '{0}' in app/web.config.", sKey));
return ConfigurationManager.AppSettings[sKey].ToString();
}
catch (Exception ex)
{
//bubble error.
throw new Exception("ConfigurationHandler::GetAppSettingsValueByKey:Error occured.", ex);
}
}
因此,我们获得了Stack API的配置信息,例如版本号,目前是2.2,以及使用的协议的初始API URL,即https://api.stackexchange.com。公共方法“GetAppSettingsValueByKey
”用于逐个键地从web.config文件中检索配置项。
创建模型
步骤(7):创建一个名为“StackClient.StackExchange.Entity”的类库。它将代表数据实体模型,或保留从Stack Exchange API检索到的数据并跨UI和数据访问层提供它的数据契约实体。
出于演示目的,我们将仅实现Question和Answer实体,另外还有一个Wrapper集合类,它可以根据其对IWrapperCollection
的实现来容纳任何实体类型的集合,如下所示。
using System;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace StackClient.StackExchange.Entity.Common
{
public interface IWrapperCollection<TEntity> where TEntity : class
{
/// <summary>
/// A list of the objects returned by the API request.
/// </summary>
[DataMember(Name = "items")]
List<TEntity> Items { get; set; }
/// <summary>
/// Whether or not <see cref="Items"/> returned by this request are the end of the pagination or not.
/// </summary>
[DataMember(Name = "has_more")]
bool? HasMore { get; set; }
/// <summary>
/// The maximum number of API requests that can be performed in a 24 hour period.
/// </summary>
[DataMember(Name = "quota_max")]
int? QuotaMax { get; set; }
/// <summary>
/// The remaining number of API requests that can be performed in the current 24 hour period.
/// </summary>
[DataMember(Name = "quota_remaining")]
int? QuotaRemaining { get; set; }
/// <summary>
/// Gets the total objects that meet the request's criteria.
/// </summary>
[DataMember(Name = "total")]
int? Total { get; set; }
}
}
因此,只有Wrapper
类将实现IWrapperCollection
,如下所示。
public class Wrapper<TEntity> : IWrapperCollection<TEntity>, IDisposable where TEntity : class
实现IDisposable接口对于内存管理很重要。此外,Question和Answer实体仅实现IDisposable接口。请参考附加的源代码。
创建存储库
步骤(8):创建一个名为“StackClient.StackExchange.Repository”的类库。它是数据访问层库(或控制器类库),它实际检索和处理从Stack API服务调用来的数据,并将其转发到其他层,在我们的例子中是用户界面层。
Repository类库只有两个类,QuestionsRepository
和AnswersRepository
,它们都实现了IRepository
接口。IRepository
代表一个通用接口,它包含了所有已实现的请求和处理Stack Exchange API的方法的签名。如下所示。
using System.Collections.Generic;
using StackClient.StackExchange.Common;
namespace StackClient.StackExchange.Repository.Common
{
public interface IRepository<TEntity> where TEntity : class
{
#region Class Methods
TEntity SelectItemById(int id);
List<TEntity> SelectItemsFiltered();
#endregion
#region Class Properties
string UrlInitialFilter { get; set; }
int? Page { get; set; }
int? PageSize { get; set; }
string Site { get; set; }
OrderType Order { get; set; }
SortType Sort { get; set; }
int? Min { get; set; }
int? Max { get; set; }
DateTime? FromDate { get; set; }
DateTime? ToDate { get; set; }
#endregion
}
}
它只有两个方法的签名,SelectItemById()
和SelectItemsFiltered()
,以及所有存储库用于过滤Stack Exchange API的常用属性;例如排序和顺序类型、From和To日期过滤器等。
QuestionsRepository
和AnswersRepository
都将引用一些命名空间,如下所示。
using System;
using System.IO;
using System.Net;
using System.Collections.Generic;
using Newtonsoft.Json;
using StackClient.StackExchange.Common;
using StackClient.StackExchange.Entity;
using StackClient.StackExchange.Repository.Common;
为了妥善处理内存资源,我们需要在存储库类被处理后立即处理实例化的日志记录和配置对象。如下所示。
private LoggingHandler _loggingHandler;
private ConfigurationHandler _configurationHandler;
private bool _bDisposed;
以及类构造函数如下所示。
public QuestionsRepository()
{
_configurationHandler = new ConfigurationHandler();
_loggingHandler = new LoggingHandler();
UrlInitialFilter = _configurationHandler.StackApiUrl;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool bDisposing)
{
// Check to see if Dispose has already been called.
if (!_bDisposed)
{
if (bDisposing)
{
// Dispose managed resources.
_configurationHandler = null;
_loggingHandler = null;
}
}
_bDisposed = true;
}
接下来,我们处理SelectItemsFiltered()
方法。首先,我们使用所需的过滤器格式化一个正确的请求URL,然后我们创建一个Web请求以获取API响应,然后最后,我们将检索到的JSON响应反序列化为特定实体(在本例中为QuestionEntity
)的包装器集合,如下所示。
public List<QuestionEntity> SelectItemsFiltered()
{
try
{
//Format the Request URL
var requestUrl = GetRequestUrlFormated();
//Send Request and Get Resulted Response Data
var responseData = RequestWebData(requestUrl);
//Now, deserialize returned data
var allData = JsonConvert.DeserializeObject<Wrapper<QuestionEntity>>(responseData);
return allData.Items;
}
catch (Exception ex)
{
//Log exception error
_loggingHandler.LogEntry(ExceptionHandler.GetExceptionMessageFormatted(ex), true);
//Bubble error to caller and encapsulate Exception object
throw new Exception("QuestionsRequest::SelectItemsFiltered::Error occured.", ex);
}
}
值得一提的是,所有检索到的响应都经过GZip或Deflate压缩。因此,我们需要在Web请求中进行处理,如下所示。
private string RequestWebData(string url)
{
try
{
var webRequest = (HttpWebRequest)WebRequest.Create(url);
//All responses are compressed, either with GZIP or DEFLATE.
webRequest.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");
webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
var response = "";
using (var webResponse = webRequest.GetResponse())
using (var sr = new StreamReader(webResponse.GetResponseStream()))
{
response = sr.ReadToEnd();
}
return response;
}
catch (WebException ex)
{
//Log exception error
_loggingHandler.LogEntry(ExceptionHandler.GetExceptionMessageFormatted(ex), true);
//Bubble error to caller and encapsulate Exception object
throw new Exception("QuestionsRequest::RequestWebData::Error occured.", ex);
}
}
最后,格式化过滤后的请求URL字符串非常简单。请参考附件以了解更多信息。
在ASP.NET中获取Stack Exchange问题
步骤(9):检索带有指向它的链接的问题
提供的示例从ASP.NET Web应用程序检索Stackoverflow网站上最新的50个问题。格式化的查询URL如下:https://api.stackexchange.com/2.2/questions?order=desc&sort=creation&page=1&pagesize=50&site=stackoverflow
回到之前创建的ASP.NET Web项目“StackClient”。现在,将Default.aspx网页的内容替换为附加代码中提供的代码,使其屏幕显示如下。
关于Default.aspx网页中的asp:GridView的一个注意事项是,它将问题显示为链接。当我们点击一个问题时,会打开一个新的网页,从stackoverflow网站检索实际问题,如下所示。
<Columns> <asp:BoundField DataField="QuestionId" HeaderText="Qs ID" SortExpression="QuestionId" > <HeaderStyle HorizontalAlign="Center"></HeaderStyle> <ItemStyle HorizontalAlign="Left" Width="60"></ItemStyle> </asp:BoundField> <asp:HyperLinkField HeaderText="Question Link" ItemStyle-Width="400" ItemStyle-Wrap="True" DataTextField="Title" DataNavigateUrlFields="Link" DataNavigateUrlFormatString="{0}" Target="_blank" /> <asp:BoundField DataField="Score" HeaderText="Qs Score" SortExpression="Score" > <HeaderStyle HorizontalAlign="Center"></HeaderStyle> <ItemStyle HorizontalAlign="Left" Width="60"></ItemStyle> </asp:BoundField> <asp:CheckBoxField DataField="IsAnswered" HeaderText="Answered" ></asp:CheckBoxField> </Columns>
请注意,“Title”通过DataNavigateUrlFields
标记为链接,Target设置为"_blank"以便在新网页中打开链接。
然后,请确保在Default.aspx页面的顶部,将jQuery UI日期选择器引用为适合你的格式,用于txtFromDate
和txtToDate
,如下所示。
<script type="text/javascript"> $(document).ready(function () { $('#<%=txtFromDate.ClientID%>').datepicker({ dateFormat: 'dd/mm/yy' }); $('#<%=txtToDate.ClientID%>').datepicker({ dateFormat: 'dd/mm/yy' }); $("#content").animate({ marginTop: "80px" }, 600); }); </script>
在Default.aspx.cs中,添加对以下命名空间的引用。
using System;
using System.Collections.Generic;
using System.Web.UI;
using StackClient.StackExchange.Common;
using StackClient.StackExchange.Entity;
using StackClient.StackExchange.Repository;
然后,创建一个私有方法,实例化QuestionsRepository数据访问类,并返回QuestionEntity
列表,如下所示。
private List<QuestionEntity> SelectAllByFactors()
{
try
{
using (var repository = new QuestionsRepository())
{
repository.Order = OrderType.Descending;
repository.Sort = SortType.Creation;
repository.Page = 1;
repository.PageSize = 50;
//repository.FromDate = set from date in UNIX format
//repository.ToDate = set to date in UNIX format
//repository.Min = set Min value
//repository.Max = set Max value
return repository.SelectItemsFiltered();
}
}
catch (Exception ex)
{
//Log exception error
_loggingHandler.LogEntry(ExceptionHandler.GetExceptionMessageFormatted(ex), true);
lblOperationResult.Text = "Sorry, loading All Questions operation failed." + Environment.NewLine + ex.Message;
}
return null;
}
然后,对于获取Stack API结果问题的按钮,只需将GridView的DataSource设置为SelectAllByFactors()
方法,如下所示。
protected void btnGetQuestions_Click(object sender, EventArgs e)
{
gvAllRecords.DataSource = SelectAllByFactors();
gvAllRecords.DataBind();
}
因此,从精简的ASP.NET客户端应用程序得到的最终结果如下。
当我们点击一个问题的链接时,它会直接带我们到Stackoverflow网站,并打开该问题的网页。
结论
本教程证明了调用REST服务与调用任何其他Web服务一样简单。JSON非常简单而且很棒。此外,为数据契约模型创建专用类库使我们能够在应用程序层中使用它,最后,我们可以轻松地在代码中应用N层架构风格,这对于提供托管代码,特别是在调用外部服务的情况下,是必不可少的。
希望你觉得这篇文章对你有帮助。