Click here to Skip to main content
14,029,821 members
Click here to Skip to main content
Add your own
alternative version

Stats

6.1K views
71 downloads
4 bookmarked
Posted 24 Nov 2017
Licenced CPOL

Calling Stack Exchange REST API from ASP.NET with N-Tier Architecture Style

, 25 Nov 2017
Rate this:
Please Sign up or sign in to vote.
ASP .NET app in C# that call Stack Exchange API and list set of Stack Questions based on some criteria.

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>
        <%--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>

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);
}

/// <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);
    }
}

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
    {
        /// <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; }
    }
}

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)
{
    // Check to see if Dispose has already been called.
    if (!_bDisposed)
    {
        if (bDisposing)
        {
            // Dispose managed resources.
            _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
    {
        //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);
    }
}

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);

        //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);
    }
}

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;
            //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;
}

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Khaled Abdelhamid
Technical Lead Freelancer
Egypt Egypt
I'm a Lead .NET consultant with large experience in Database (SQL server, Oracle, MySQL), Web Services, WCF & REST and I'm excellent coder of C# with over 15 years of international commercial working experience by working in different environments and countries such as Egypt, UK, Qatar and Bahrain.

You may also be interested in...

Pro

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web02 | 2.8.190419.4 | Last Updated 26 Nov 2017
Article Copyright 2017 by Khaled Abdelhamid
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid