Dec
20
Written by:
Michael Washington
12/20/2018 10:03 PM
Calling a REST based API from .Net Core is usually more complicated than many of the samples you may have seen. The reason is that each REST based API can implement its own unique security measures. In addition, passing complex parameters, and uploading files though a REST based API, can prove challenging.
In this example, we will look at consuming the REST Based API for the popular open source Help Desk application ADefHelpDesk.com. This API has the following features that we will explore:
- Requires callers to first obtain a JSON Web Token (JWT) (also called a Bearer Token), using a combination of a UserName, Password, and Application GUID. The token is then passed in the header of the subsequent requests to the other methods to authenticate the requests.
- Allows external caller to create Help Desk Tickets (and perform all other functionality).
- As part of creating Help Desk Tickets, the API allows the external caller to upload files.
Install ADefHelpDesk
You can download and install ADefHelpDesk from the site at: http://ADefHelpDesk.com.
After you have the site running, follow the directions at the link: API Security (Swagger Rest API) on the documentation page that can be found at this link to create an account that can call the API.
You will need to copy the information on the Connection Information tab to use later.
In the ADefHelpDesk site there is a Swagger page (see: https://swagger.io/) that documents the REST based API endpoints at: http://{your default web address}/swagger/ (for example: http://adefhelpdesk.azurewebsites.net/swagger/).
We will refer to this page frequently.
Create The Application
Open Visual Studio.
Select Web, then ASP.NET Core Web Application.
Enter AdefHelpDeskMVC for the Name.
Press OK.
Select the Web Application template and press OK.
The project will be created.
Load Application Settings
In the Solution Explorer, open the appsettings.json file.
Replace the contents with the following (replacing the settings with your own values gathered earlier):
{
"AllowedHosts": "*",
"CustomSettings": {
"APIWebAddress": "** Your Web Address **",
"ApplicationGUID": "** Your ApplicationGUID **",
"UserName": "** Your UserName **",
"Password": "** Your Password **"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}
Add a folder called Models and a class file called CustomSettings.cs using the following code:
namespace AdefHelpDeskMVC.Models
{
// This allows us to retrieve custom settings
// from appsettings.json
public class CustomSettings
{
public string APIWebAddress { get; set; }
public string ApplicationGUID { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
}
Next, open the Startup.cs file.
Change the Startup method to the following:
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
Change the ConfigureServices method to the following:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// Get the CustomSettings from appsettings.json
// allow them to be passed to any class using dependency injection
services.Configure<Models.CustomSettings>(
Configuration.GetSection("CustomSettings"));
}
This will load the settings in the appsettings.json file and make them available, through the CustomSettings class, using dependency injection.
Get The JSON Web Token (JWT) (Authentication Token)
The ADefHelpDesk API requires that you use the UserName and Password to call the /api/V1/GetAuthToken API method, to obtain a JSON Web Token (JWT) to use as authentication for subsequent calls to the other API methods.
Note that the method has the word Post in the green box. This indicates that it must be called by sending a Post request.
Note that the JWT will expire after a few minutes, and when it does, you will have to call the /api/V1/GetAuthToken API method again to obtain another JWT.
So, as the first step, we will create code to allow a JWT to be retrieved.
The Swagger page will provide the format (the class definition) of the parameter the endpoint requires.
Create a class file called DTOApiToken.cs using the following code:
namespace AdefHelpDeskMVC.Models
{
public class DTOApiToken
{
public string userName { get; set; }
public string password { get; set; }
public string applicationGUID { get; set; }
}
}
Next, open the page at Pages/Index.cshtml and change all the code to the following:
@page
@model IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData["Title"] = "ADefHelpDesk API Client";
}
<div class="row">
<div class="col-md-12">
<h4>ADefHelpDesk API Client</h4>
<p class="clearfix"><b>@Model.Message</b></p>
</div>
</div>
<div class="row">
<form asp-page-handler="AuthToken" method="post">
<button class="btn btn-default">Get Auth Token</button>
</form>
</div>
<div class="row">
<p class="col-md-12">@Model.BearerToken</p>
</div>
<br />
This creates the page layout with a button that will retrieve the Auth Token (the JWT) and a label to display it.
Finally, open the Index.cshtml.cs file and change all the code to the following:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using AdefHelpDeskMVC.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace AdefHelpDeskMVC.Pages
{
[BindProperties]
public class IndexModel : PageModel
{
// Used to make REST calls
public static HttpClient client;
// To store the settings from the appsettings.json file
private CustomSettings _CustomSettings;
// To hold any error messages
public string Message { get; set; }
// To store the JWT (Authentication token)
public string BearerToken { get; set; }
// This method is called when this class is initialized
public IndexModel(IOptions<CustomSettings> CustomSettings)
{
// Get the custom settings using dependency injection
_CustomSettings = CustomSettings.Value;
}
}
}
This provides the code to load the CustomSettings, using dependency injection, that were registered in the Startup.cs file.
Add the following methods:
private void InitializeForm()
{
// Initialization code to be added later...
}
// This method called when the page is loaded
public void OnGet()
{
InitializeForm();
}
Now, to implement the code to handle the button click (to retrieve the JWT), add the following method:
public async void OnPostAuthTokenAsync()
{
// Instantiate the DTOApiToken class and set the parameters
// using the values from CustomSettings
DTOApiToken paramApiToken = new DTOApiToken();
paramApiToken.userName = _CustomSettings.UserName;
paramApiToken.password = _CustomSettings.Password;
paramApiToken.applicationGUID = _CustomSettings.ApplicationGUID;
// Call the GetAuthToken method and retrieve the BearerToken (JWT - Auth Token)
// The BearerToken will then display on the page because it has
// the following code: @Model.BearerToken
BearerToken = await GetAuthToken(_CustomSettings.APIWebAddress, paramApiToken);
InitializeForm();
}
Finally, add the following code to implement the GetAuthToken method:
private static async Task<string> GetAuthToken(string APIWebAddress, DTOApiToken paramApiToken)
{
// Store the final result
string strResult = "";
// Use the HttpClient
using (client)
{
// Initialize the HttpClient
client = new HttpClient();
// Create a new REST request
using (var request = new HttpRequestMessage())
{
// The Swagger page indicates this must be a "Post"
request.Method = HttpMethod.Post;
// Set the destination to the method indicated on the Swagger page
// to: api/V1/GetAuthToken
request.RequestUri = new Uri($"{APIWebAddress}api/V1/GetAuthToken");
// Convert the parameters to JavaScript Object Notation (JSON) format
var json = JsonConvert.SerializeObject(paramApiToken);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
// Make the request to the API endpoint on the ADefHelpDesk site
var response = client.SendAsync(request).Result;
// Receive the response
var JsonDataResponse =
await response.Content.ReadAsStringAsync();
// Convert the response (the JWT (Auth Token)) to a String value
strResult =
JsonConvert.DeserializeObject<string>(JsonDataResponse);
// Strip the word Bearer from the token
strResult = strResult.Replace("Bearer ", " ");
}
}
// Return the JWT
return strResult;
}
Hit F5 to run the project.
You can now click the Get Auth Token button and the code will call the API method on the ADefHelpDesk site, passing the UserName, Password, and ApplicationGUID, and retrieve the JWT that will then be displayed on the page.
Retrieve The Current User
To demonstrate how the JWT is used to authenticate a request, we will call the GetCurrentUser API method.
When we look at the Swagger page we can get the API endpoint address, see that we call it as a Get, and that we pass no parameters.
However, we will need to pass an valid and unexpired JWT in the header of the request, otherwise we will get an unauthorized response.
Add the following code to the Index.cshtml page:
<div class="row">
<form asp-page-handler="CurrentUser" method="post">
<input type="hidden" name="paramBearerToken" value="@Model.BearerToken" />
<button class="btn btn-default">Get Current User</button>
</form>
</div>
<div class="row">
<p class="col-md-12">@Model.CurrentAPIUser</p>
</div>
This will create a button to click to retrieve the name of the API User and a div to display it.
Note that there is a hidden field called paramBearerToken that contains the value of the JWT. Because it is in the form tag it will be passed to the CurrentUser method specified in the asp-page-handler tag.
Add the following code to the Index.cshtml.cs page:
// Store the name of the current API User
public string CurrentAPIUser { get; set; }
Next, add the following method to handle the button click:
public async void OnPostCurrentUserAsync(string paramBearerToken)
{
// paramBearerToken is passed to this method (when the button is clicked)
// by the hidden field
if (paramBearerToken == null)
{
// Set error message
Message = "The Auth Token is required";
}
else
{
// Call the API
var response =
await GetCurrentUser(_CustomSettings.APIWebAddress, paramBearerToken);
if (response == "Unauthorized")
{
// Set error message
Message = "The Auth Token is expired or invalid";
}
else
{
// Return the Current User
CurrentAPIUser = response;
// Save the Bearer Token
BearerToken = paramBearerToken;
}
}
InitializeForm();
}
Finally, add the following code to implement the GetCurrentUser method that is being called by the code above:
private static async Task<string> GetCurrentUser(string APIWebAddress, string paramBearerToken)
{
// Store the final result
string strResult = "";
// Use the HttpClient
using (client)
{
// Initialize the HttpClient
client = new HttpClient();
// Create a new REST request
using (var request = new HttpRequestMessage())
{
// The Swagger page indicates this must be a "Post"
request.Method = HttpMethod.Get;
// Set the destination to the method indicated on the Swagger page
// to: api/V1/GetCurrentUser
request.RequestUri = new Uri($"{APIWebAddress}api/V1/GetCurrentUser");
// Pass the JWT in the 'header' of the request with the word "Bearer" in front
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", paramBearerToken);
// Make the request to the API endpoint on the ADefHelpDesk site
var response = client.SendAsync(request).Result;
// Handle if the JWT is expired
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
return "Unauthorized";
}
// Receive the response
var JsonDataResponse =
await response.Content.ReadAsStringAsync();
// Convert the response to a String value
strResult =
JsonConvert.DeserializeObject<string>(JsonDataResponse);
}
}
// Return the response
return strResult;
}
When we run the application, we first click the Get Auth Token button to get a JWT, then we click the Get Current User button to pass the JWT to the method that will retrieve the name of the current API user.
Create A Help Desk Ticket
We will now create a Help Desk Ticket by calling the CreateTask method.
We see that this one has a lot of parameters.
The parameters are a combination of the DTOTask class and the DTOTaskDetail class.
Their full definition for the classes is available by scrolling down toward the bottom of the Swagger page.
We can also see the class that the method will return.
To represent these classes, add three class files to the Models folder using the following code:
DTOStatus.cs
namespace AdefHelpDeskMVC.Models
{
public class DTOStatus
{
public string StatusMessage { get; set; }
public bool Success { get; set; }
}
}
DTOTask.cs
using System.Collections.Generic;
namespace AdefHelpDeskMVC.Models
{
public class DTOTask
{
public int? taskId { get; set; }
public int? portalId { get; set; }
public string description { get; set; }
public string status { get; set; }
public string priority { get; set; }
public string createdDate { get; set; }
public string estimatedStart { get; set; }
public string estimatedCompletion { get; set; }
public string dueDate { get; set; }
public int? assignedRoleId { get; set; }
public string assignedRoleName { get; set; }
public string ticketPassword { get; set; }
public int? requesterUserId { get; set; }
public string requesterName { get; set; }
public string requesterEmail { get; set; }
public string requesterPhone { get; set; }
public int? estimatedHours { get; set; }
public bool? sendEmails { get; set; }
public List<int> selectedTreeNodes { get; set; }
public List<DTOTaskDetail> colDTOTaskDetail { get; set; }
}
}
DTOTaskDetail.cs
using System.Collections.Generic;
namespace AdefHelpDeskMVC.Models
{
public class DTOTaskDetail
{
// Note: this field is renamed from description to
// taskDetailDescription to prevent collision when passed
// as a parameter with the field description in the Task object
public string taskDetailDescription { get; set; }
public int detailId { get; set; }
public string detailType { get; set; }
public string contentType { get; set; }
public string insertDate { get; set; }
public int userId { get; set; }
public string userName { get; set; }
public string emailDescription { get; set; }
public string startTime { get; set; }
public string stopTime { get; set; }
public bool? sendEmails { get; set; }
}
}
To create a form that will allow the Ticket to be input, add the following code to the Index.cshtml page:
<div>
<form asp-page-handler="CreateTicket" method="post" enctype="multipart/form-data">
<input type="hidden" name="paramBearerToken" value="@Model.BearerToken" />
<div class="row">
<p class="col-md-1">Name:</p>
<p class="col-md-11">
<input asp-for="@Model.TicketForm.Name" /></p>
</div>
<div class="row">
<p class="col-md-1">Email:</p>
<p class="col-md-11">
<input type="email" asp-for="@Model.TicketForm.Email" /></p>
</div>
<div class="row">
<p class="col-md-1">Phone:</p>
<p class="col-md-11">
<input type="tel" asp-for="@Model.TicketForm.Phone" /></p>
</div>
<div class="row">
<p class="col-md-1">Description:</p>
<p class="col-md-11">
<input asp-for="@Model.TicketForm.Description" /></p>
</div>
<div class="row">
<p class="col-md-1">Due Date:</p>
<p class="col-md-11">
<input type="date" asp-for="@Model.TicketForm.DueDate" /></p>
</div>
<div class="row">
<p class="col-md-1">Priority:</p>
<p class="col-md-11">
<select asp-for="@Model.TicketForm.Priority" asp-items="Model.Options"></select></p>
</div>
<div class="row">
<button class="btn btn-default">Create Help Desk Ticket</button>
</div>
</form>
</div>
Add the following class (outside of the IndexModel class but inside the AdefHelpDeskMVC namespace), to hold the properties used in the form, to the Index.cshtml.cs file:
public class Ticket
{
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Description { get; set; }
public string Detail { get; set; }
public DateTime? DueDate { get; set; }
public string Priority { get; set; }
}
Add this extension method class that will be used later to serialize parameters to append to the URL:
public static class ObjectExtensions
{
// From: https://bit.ly/2A5R70I
public static IDictionary<string, string> ToKeyValue(this object metaToken)
{
if (metaToken == null)
{
return null;
}
JToken token = metaToken as JToken;
if (token == null)
{
return ToKeyValue(JObject.FromObject(metaToken));
}
if (token.HasValues)
{
var contentData = new Dictionary<string, string>();
foreach (var child in token.Children().ToList())
{
var childContent = child.ToKeyValue();
if (childContent != null)
{
contentData = contentData.Concat(childContent)
.ToDictionary(k => k.Key, v => v.Value);
}
}
return contentData;
}
var jValue = token as JValue;
if (jValue?.Value == null)
{
return null;
}
var value = jValue?.Type == JTokenType.Date ?
jValue?.ToString("o", CultureInfo.InvariantCulture) :
jValue?.ToString(CultureInfo.InvariantCulture);
return new Dictionary<string, string> { { token.Path, value } };
}
}
Add the following properties inside the IndexModel class:
// To hold values for the Ticket form
public Ticket TicketForm { get; set; }
// To hold values for the dropdown on the Ticket form
public List<SelectListItem> Options { get; set; }
Change the InitializeForm method to the following:
private void InitializeForm()
{
// Instatiate the values for the form
TicketForm = new Ticket();
// Create options for the dropdown on the form
Options = new List<SelectListItem>();
Options.Add(new SelectListItem { Value = "Normal", Text = "Normal" });
Options.Add(new SelectListItem { Value = "High", Text = "High" });
Options.Add(new SelectListItem { Value = "Low", Text = "Low" });
// Set the default Priority value
TicketForm.Priority = "Normal";
}
Next, add the method that will handle the button click when the form is submitted:
public async void OnPostCreateTicketAsync(string paramBearerToken)
{
if (paramBearerToken == null)
{
// Set error message
Message = "The Auth Token is required";
}
else
{
// Format the fields
DTOTask objDTOTask = new DTOTask();
DTOTaskDetail objDTOTaskDetail = null;
IFormFile objIFormFile = null;
objDTOTask.createdDate = DateTime.Now.ToShortDateString();
objDTOTask.requesterUserId = -1;
objDTOTask.status = "New";
objDTOTask.sendEmails = true;
// Set values from form
objDTOTask.description = TicketForm.Description;
objDTOTask.dueDate = TicketForm.DueDate?.ToShortDateString();
objDTOTask.priority = TicketForm.Priority;
objDTOTask.requesterEmail = TicketForm.Email;
objDTOTask.requesterName = TicketForm.Name;
objDTOTask.requesterPhone = TicketForm.Phone;
// Call the API
var response = await CreateTicket(
objDTOTask,
objDTOTaskDetail,
objIFormFile,
_CustomSettings.APIWebAddress,
paramBearerToken);
if (response.StatusMessage == "Unauthorized")
{
// Set error message
Message = "The Auth Token is expired or invalid";
}
else
{
// Save the Bearer Token
BearerToken = paramBearerToken;
if (response.Success)
{
Message = "Ticket Created!";
}
else
{
Message = response.StatusMessage;
}
}
}
InitializeForm();
}
Finally, add the code to implement the CreateTicket method that the code above calls:
private static async Task<DTOStatus> CreateTicket(
DTOTask paramDTOTask,
DTOTaskDetail paramDTOTaskDetail,
IFormFile paramIFormFile,
string APIWebAddress,
string paramBearerToken)
{
HttpResponseMessage response;
DTOStatus Result = new DTOStatus();
using (client)
{
client = new HttpClient();
using (var request = new HttpRequestMessage())
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", paramBearerToken);
// Add Task parameters
var TaskParameters = paramDTOTask.ToKeyValue();
// If there are Task Details add them
if (paramDTOTaskDetail != null)
{
var TaskDetailParameters = paramDTOTaskDetail.ToKeyValue();
foreach (var item in TaskDetailParameters)
{
TaskParameters.Add(item);
}
}
request.RequestUri =
new Uri(QueryHelpers.AddQueryString(
$"{APIWebAddress}api/V1/CreateTask"
, TaskParameters));
request.Method = HttpMethod.Post;
// Send request
response = client.SendAsync(request).Result;
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Result.Success = false;
Result.StatusMessage = "Unauthorized";
return Result;
}
var JsonDataResponse =
await response.Content.ReadAsStringAsync();
Result =
JsonConvert.DeserializeObject<DTOStatus>(JsonDataResponse);
}
}
return Result;
}
Note, the IFormFile paramIFormFile parameter is being passed but is not being implemented yet. We will use it to upload a file in the final section of this tutorial.
When we run the project, we can obtain an Auth Token, fill out the form, and submit a new Help Desk Ticket.
If we log into the ADefHelpDesk application, we can see the new Ticket.
Note: In this instance, the user specified in the Email parameter would be emailed a special link to the Ticket that allowed them to view and add comments to it. You can set a property to suppress emails on a per Ticket basis if needed. You also have the option to call the API method to create an account for a user and connect the Ticket to that account.
Upload A File
The ADefHelpDesk API allows a file to be uploaded when creating (or updating) a Help Desk Ticket.
It requires the file to be attached to a Task Detail object.
To hold the optional Detail text for the Task Detail object, add the following code to the form on the Index.cshtml page:
<div class="row">
<p class="col-md-1">Detail:</p>
<p class="col-md-11">
<textarea asp-for="@Model.TicketForm.Detail"></textarea></p>
</div>
Also, add the following code to the form to allow a file to be uploaded:
<div class="row">
<p class="col-md-1">File:</p>
<p class="col-md-11">
<input type="file" asp-for="@Model.TicketForm.FormFile" /></p>
</div>
In the Index.cshtml.cs file, add the following property to the Ticket class to hold the value of the file:
public IFormFile FormFile { get; set; }
Add the following code to the OnPostCreateTicketAsync method:
// If there is a file we need a Task Detail
if (TicketForm.FormFile != null)
{
objDTOTaskDetail = new DTOTaskDetail();
// Note: this field is renamed from description to
// taskDetailDescription to prevent collision when passed
// as a parameter with the field description in the Task object
objDTOTaskDetail.taskDetailDescription = TicketForm.FormFile.FileName;
// Set File
objIFormFile = TicketForm.FormFile;
}
Finally, in the CreateTicket method, remove the line:
response = client.SendAsync(request).Result;
Replace it with:
if (paramIFormFile != null)
{
using (var readStream = paramIFormFile.OpenReadStream())
{
MultipartFormDataContent _multiPartContent =
new MultipartFormDataContent();
// Serialize Request
StreamContent _fileData =
new StreamContent(readStream);
_fileData.Headers.ContentType =
new MediaTypeHeaderValue("application/octet-stream");
ContentDispositionHeaderValue _contentDispositionHeaderValue =
new ContentDispositionHeaderValue("form-data");
_contentDispositionHeaderValue.Name = "objFile";
_contentDispositionHeaderValue.FileName = paramIFormFile.FileName;
_fileData.Headers.ContentDisposition = _contentDispositionHeaderValue;
_multiPartContent.Add(_fileData, "objFile");
// Set content
request.Content = _multiPartContent;
// Send request
response = client.SendAsync(request).Result;
}
}
else
{
// Send request
response = client.SendAsync(request).Result;
}
Now we can upload a file when creating a new Help Desk Ticket.
If we upload a .EML file we can see its contents displayed in ADefHelpDesk, and any attachments in the .EML file will have download links.
If we upload a file that is not a .EML file, that file will display a download link.
Advantages Of A REST API
The ADefHelpDesk API demonstrates the power of using REST APIs. Rather than having multiple Help Desk applications to support your various applications, that require your users to exit the application to create a Ticket, you can use the API to allow your users to create tickets from inside those applications.
The API provides methods that will allow you to create a user account for the user, if needed, so their tickets can be tracked, and email notifications sent.
This allows a single installation of ADefHelpDesk to support unlimited applications, yet your support personnel manages it all from one interface. This also allows processes such as auto responders to periodically check for new tickets, and update, and route them.
Links
AdefHelpDesk (Documentation)
A Few Great Ways to Consume RESTful API in C#
https://www.asp.net/web-api
Download
The project is available at http://lightswitchhelpwebsite.com/Downloads.aspx
You must have Visual Studio 2017 (or higher) installed to run the code.