Introduction
It is not very common to find documentation on how to implement the Stack Exchange (Stackoverflow) API https://api.stackexchange.com questions through a custom developed web application. So, it is a good opportunity to spot the light on how to use it from a thin client web app, ASP .NET web app.
I’m going to create a complete ASP .NET solution in C# that interact with Stack Exchange API and list set of Stack questions based on some criteria. Stack Exchange is a question and answer site for meta-discussion of the Stack Exchange family of Q&A websites. Stack Exchange API is REST service that takes an URI Query String request based on HTTP/HTTPS and returns JSON/JSONP response enabling users to retrieve questions, answers, comments, badges, events, revisions, suggested edits, user information, and tags from the site.
All responses are compressed, either with GZIP or DEFLATE. Stack Exchange API implements OAuth 2.0 for authentication in case you have registered your client app with the Stack Exchange site. It is possible to compose reasonably complex queries against the live Stack Exchange sites using the min, max, fromdate, todate, and sort parameters.
Applying Design Principles
This tutorial adopts an N-Tier architecture style in implementing the Stack Exchange REST service. Meanwhile, we can apply the same approach when dealing with database via Data Access Layer. All of you are familiar with the N-Tier architecture style. However, do you know how to implement it into your code in an appropriate manner? This tutorial answers this question too.
The below figure shows the building blocks of the architecture we’re going to implement. So, let’s start our demo defining component-by-component. We start here by the Model component; the Model simply, as you may know, is a class with properties mapped to a database table with its column.
The figure shows the Model component separated as a crosscutting class library that is going to interact with the three main layers of the N-Tier architect, UI Presentation Layer, Business Logic Layer, and the Data Access Layer. This shall simplify dealing with the Entity Model from within any of these layers. There are other crosscutting components, helper components, which provide configuration, logging, exception handling, and other stuff into our app. These components are mandatory in case we build our solutions in a professional way.
We normally need the Business Logic Layer in cases where our solution handles many business constraints, such as salary constraints in an ERP System, but this is not the case in here. So, we’re going to neglect the use of a business layer. We’re going to omit it from our solution. So, we’re going to deal with and consume the external Stack Exchange API from the repository classes inside our Data Access Layer.
Now, let’s go and build our solution. I’ve used VS 2013 with FW 4.5.2 to build the solution, and the following steps are the sequence of steps I used to do in building this demo App.
Creating the UI Presentation Layer
Step (1): Create ASP .Net Web Forms Project and name it StackClient
Step (2): Open the Web.Config file and add the app settings as shown in the below figure
<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>
The StackApiVersion
keeps the current version of the Stack Exchange REST service version, which is 2.2. The StackApiUseHttps
identifies whether to use HTTP or HTTPS protocol. The EnableLogging
is set to TRUE to enable logging for testing purposes, and so on.
Step (3): Update your jQuery library by installing the latest version NuGet package
Step (4): Install the jQuery-ui NuGet
Step (5): Add the jQuery and jQuery-ui versions to the head section of the Site.master page as shown below.
<head runat="server">
…
<link href="~/Content/themes/base/jquery-ui.css" rel="stylesheet" />
</head>
And inside the form body of the Site.master page
<asp:ScriptManager runat="server">
<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" />
<%----%>
</Scripts>
</asp:ScriptManager>
Creating Crosscutting Helper Components
Step (6): Create a Class Library called “StackClient.StackExchange.Common”. It shall represents the crosscutting helper components. We create the “ConfigurationHandler”, “ExceptionHandler”, “LoggingHandler”, “FileHandler”, and the “EnumHandler” C# files. Please refer to the attached solution source code to locate them.
The point to mention here is that the ConfigurationHandler
class shall get configuration settings from the web.config file upon instantiation as shown in the below C# code.
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);
}
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)
{
throw new Exception("ConfigurationHandler::GetAppSettingsValueByKey:Error occured.", ex);
}
}
Hence, we get the Stack API configuration information such as the Version number, which is 2.2 for now, and initial API URL with the used protocol, which is https://api.stackexchange.com. The public method “GetAppSettingsValueByKey
” is used to retrieve configuration items from the web.config file key-by-key.
Creating the Models
Step (7): Create a Class Library called “StackClient.StackExchange.Entity”. It shall represents the Data Entity Model, or the Data Contract Entities that retain the retrieved data from the Stack Exchange API and provide it across the UI and Data Access Layers.
For the demo purpose, we shall implement the Question and Answer entities only, in addition to a Wrapper collection class that can hold a collection any Entity type according to its implementation to the IWrapperCollection
as shown below
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
{
[DataMember(Name = "items")]
List<TEntity> Items { get; set; }
[DataMember(Name = "has_more")]
bool? HasMore { get; set; }
[DataMember(Name = "quota_max")]
int? QuotaMax { get; set; }
[DataMember(Name = "quota_remaining")]
int? QuotaRemaining { get; set; }
[DataMember(Name = "total")]
int? Total { get; set; }
}
}
Hence, only the Wrapper
class that is going to implement the IWrapperCollection
as follows
public class Wrapper<TEntity> : IWrapperCollection<TEntity>, IDisposable where TEntity : class
The implementation of IDisposable interface is important for memory management. In addition, the Question and Answer entities only implement the IDisposable interface. Please refer back to the attached source code.
Creating the Repository
Step (8): Create a Class Library called “StackClient.StackExchange.Repository”. It is the Data Access Layer library (or the Controller class library) that actually retrieve and handle the called data from the Stack API service and forward it to the other layers, in our case, the user interface layer.
The Repository class library has only two classes, the QuestionsRepository
and the AnswersRepository
, and both of them implement the IRepository
interface. The IRepository
represents a generic interface that holds the signature of all implemented methods requesting and handling the Stack Exchange API. It is as shown below
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
}
}
It has signature of only two methods, the SelectItemById()
and the SelectItemsFiltered()
plus the common properties used by all repositories for filtering the Stack Exchange API; such as the sort and order types, the From and To dates filters, etc.
Both of the QuestionsRepository
and AnswersRepository
shall reference a number of namespaces, as shown below
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;
To handle the memory resources properly, we need to dispose the instantiated logging and configuration objects as soon as the repository class is disposed. This is clarified below.
private LoggingHandler _loggingHandler;
private ConfigurationHandler _configurationHandler;
private bool _bDisposed;
And the class constructor to be as follows
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)
{
if (!_bDisposed)
{
if (bDisposing)
{
_configurationHandler = null;
_loggingHandler = null;
}
}
_bDisposed = true;
}
Next, we handle the SelectItemsFiltered()
method. Firstly, we format a proper request URL with the filters we need, then we create web request to get the API response, then finally, we deserialize the retrieved JSON response into a wrapper collection of the specific entity, QuestionEntity
in our case, as shown below.
public List<QuestionEntity> SelectItemsFiltered()
{
try
{
var requestUrl = GetRequestUrlFormated();
var responseData = RequestWebData(requestUrl);
var allData = JsonConvert.DeserializeObject<Wrapper<QuestionEntity>>(responseData);
return allData.Items;
}
catch (Exception ex)
{
_loggingHandler.LogEntry(ExceptionHandler.GetExceptionMessageFormatted(ex), true);
throw new Exception("QuestionsRequest::SelectItemsFiltered::Error occured.", ex);
}
}
It worth mentioning here that all the retrieved responses are compressed with either GZip or Deflate. So, we need to handle them in the web request as shown below.
private string RequestWebData(string url)
{
try
{
var webRequest = (HttpWebRequest)WebRequest.Create(url);
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)
{
_loggingHandler.LogEntry(ExceptionHandler.GetExceptionMessageFormatted(ex), true);
throw new Exception("QuestionsRequest::RequestWebData::Error occured.", ex);
}
}
Finally, formatting the filtered request URL string is quite easy. Please, reference the attached for revealing it.
Getting Stack Exchange Questions in ASP.NET
Step (9): Retrieve Questions with links point to it
The provided sample retrieve the latest 50 questions posted on Stackoverflow site from the ASP .NET web app. The formulated query URL is shown in the below URL: https://api.stackexchange.com/2.2/questions?order=desc&sort=creation&page=1&pagesize=50&site=stackoverflow
Go back to the ASP.NET web project, the “StackClient” created earlier. Now, replace the content of the Default.aspx web page with the content provided in the attached code for the screen to be as shown below
One note upon the asp:GridView, in the Default.aspx web page is that it shall present questions as links. When we click on a Question, a new web page is opened retrieving the actual question from the stackoverflow web site, as shown in below
<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>
Note that, the “Title” is marked up with links with the DataNavigateUrlFields
and the Target is set to "_blank" in order to open link in a new web page.
Then, make sure to reference the jQuery UI date picker with the appropriate format you like, for both of the txtFromDate
and the txtToDate
in the top of the Default.aspx page as shown below
<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>
In the Default.aspx.cs, add reference to the following namespaces
using System;
using System.Collections.Generic;
using System.Web.UI;
using StackClient.StackExchange.Common;
using StackClient.StackExchange.Entity;
using StackClient.StackExchange.Repository;
Then, create a private method that instantiate the QuestionsRepository Data Access class and returns a list of QuestionEntity
as shown below
private List<QuestionEntity> SelectAllByFactors()
{
try
{
using (var repository = new QuestionsRepository())
{
repository.Order = OrderType.Descending;
repository.Sort = SortType.Creation;
repository.Page = 1;
repository.PageSize = 50;
return repository.SelectItemsFiltered();
}
}
catch (Exception ex)
{
_loggingHandler.LogEntry(ExceptionHandler.GetExceptionMessageFormatted(ex), true);
lblOperationResult.Text = "Sorry, loading All Questions operation failed." + Environment.NewLine + ex.Message;
}
return null;
}
Then, for the button that gets resulted Stack API Questions, just set the DataSource of the GridView to the SelectAllByFactors()
method, as shown below.
protected void btnGetQuestions_Click(object sender, EventArgs e)
{
gvAllRecords.DataSource = SelectAllByFactors();
gvAllRecords.DataBind();
}
Hence, the final result from the thin client ASP.NET app shall be as follows
And when we click on a link of a Question, it directly takes us to the Stackoverflow site and opens the web page of that Question.
Conclustion
This tutorial proved that calling a REST service is as simple as calling any other web service. JSON is quite simple and brilliant. Moreover, creating a dedicated class library for the Data Contract Model enables us to use it our app layers, and finally and we can easily apply N-Tier Architecture Style in our code, which is mandatory to provide managed code especially in the case of calling external services.
I hope you find this article helpful to you.