Consuming ASP.NET Web API
One of the greatest benefits of ASP.NET Web API is its reachability. A broad range of clients in disparate platforms can consume your web API, leveraging the support HTTP enjoys across platforms and devices. In this chapter, I cover the topic of client applications consuming your web API. I’ve limited the coverage to .NET clients—a console application, a Windows Presentation Foundation (WPF) application, and a JavaScript client running in the context of a browser.
11.1 Calling a Web API from a Console Application
In this exercise, you will call your web API from a C# console application. To call the web API, you can use System.Net.WebClient, or even WebRequest and WebResponse classes, or for that matter any other library you are familiar with. You just need HTTP capabilities to call a web API. However, this example will use HttpClient, the modern HTTP client that is available from the .NET framework version 4.5 (ported to .NET 4.0 as well). You can issue more than one request through a single HttpClient instance, and you get the async features of .NET 4.5 by default, helping you manage and coordinate multiple requests.
Listing 11-1. The Console Application Main Method
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using Robusta.TalentManager.WebApi.Dto;
class Program
{
static void Main(string[] args)
{
HttpClient client = new HttpClient();
client.BaseAddress = new Uri(" http://localhost/TalentManager/api/ ");
HttpResponseMessage response = client.GetAsync("employees/1").Result;
Console.WriteLine("{0} - {1}", (int)response.StatusCode, response.ReasonPhrase);
if (response.IsSuccessStatusCode)
{
var employee = response.Content.ReadAsAsync<EmployeeDto>().Result;
Console.WriteLine("{0} {1} {2}",
employee.Id,
employee.FirstName,
employee.LastName,
employee.DepartmentId);
}
} // Add a breakpoint on this line
}
Listing 11-2. POSTing an Employee
EmployeeDto newEmployee = new EmployeeDto()
{
FirstName = "Julian",
LastName = "Heineken",
DepartmentId = 2
};
response = client.PostAsJsonAsync<EmployeeDto>
(" http://localhost/TalentManager/api/employees ", newEmployee)
.Result;
if (response.IsSuccessStatusCode)
{
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
if (response.Headers != null)
Console.WriteLine(response.Headers.Location);
}
Console.WriteLine("{0} - {1}", (int)response.StatusCode, response.ReasonPhrase);
HttpClient makes it easy to add the request headers using strongly typed APIs instead of having to work with strings all the time.
Listing 11-3. Adding Accept Headers
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json", 0.8));
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/xml", 0.9));
response = client.GetAsync("employees/1").Result;
Console.WriteLine("{0} - {1}", (int)response.StatusCode, response.ReasonPhrase);
if (response.IsSuccessStatusCode)
{
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
}
[Authorize]
public HttpResponseMessage Get(int id) { ... }
[Authorize]
public HttpResponseMessage Post(EmployeeDto employeeDto) { ... }
The Authorize attribute expects authenticated identity, and we are not passing any credentials for the message handlers to establish identity. To send the credentials, we could add the code directly in the Main method. Instead, let us use a client-side message handler. Just as on the server side, you can have message handlers on the client side as well.
Listing 11-4. The Client Message Handler
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
public class CredentialsHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var headers = request.Headers;
if (headers.Authorization == null)
{
string creds = String.Format("{0}:{1}", "jqhuman", "p@ssw0rd!");
byte[] bytes = Encoding.Default.GetBytes(creds);
var header = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(bytes));
headers.Authorization = header;
}
return await base.SendAsync(request, cancellationToken);
}
}
HttpClient client = HttpClientFactory.Create(new CredentialsHandler());
We use HttpClientFactory to create the client instance in order to specify the handler to be run in the client pipeline.
Note Because we set the header from the message handler, the header is set for every request, with the handler being called every time we make a web API call. If you set the header using the DefaultRequestHeaders property of HttpClient, setting it once is enough.
11.2 Calling a Web API from a WPF Application
In this exercise, you will call your web API from a WPF application using HttpClient. The code you will write for calling the web API will be very similar to Exercise 11.1. However, you will see how to write non-blocking code to take advantage of the async features of .NET 4.5.
If you are not familiar with the Model-View-View-Model (MVVM) pattern typically followed with WPF applications, the code in this exercise might appear different from what you are used to, but the idea is to separate the concerns and make the view models completely unit-testable. So you will not find any code in the view (the code-behind file of the XAML) except the call to the InitializeComponent method. We use MVVM mainly to keep the code clean, and the MVVM pattern is not mandatory for consuming ASP.NET Web API from your WPF application.
Listing 11-5. The ViewModelBase Class
using System;
using System.ComponentModel;
using System.Linq.Expressions;
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpresssion)
{
string propertyName = GetPropertyName(propertyExpresssion);
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private string GetPropertyName<T>(Expression<Func<T>> propertyExpresssion)
{
string propertyName = String.Empty;
MemberExpression expression = propertyExpresssion.Body as MemberExpression;
if (expression != null)
{
propertyName = expression.Member.Name;
}
return propertyName;
}
}
Listing 11-6. The RelayCommand Class
using System;
using System.Windows.Input;
public class RelayCommand : ICommand
{
private readonly Action<object> action;
private readonly Predicate<object> canExecute;
public RelayCommand(Action<object> execute) : this(execute, null) { }
public RelayCommand(Action<object> execute, Predicate<object> canExecute)
{
action = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute == null ? true : canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
action(parameter);
}
}
Figure 11-1. The EmployeeFind window
Listing 11-7. The EmployeeFind XAML
<Window x:Class="Robusta.TalentManager.WebApi.Client.WinApp.Views.EmployeeFind"
xmlns=" http://schemas.microsoft.com/winfx/2006/xaml/presentation "
xmlns:x=" http://schemas.microsoft.com/winfx/2006/xaml "
Title="Find Employee" Height="300" Width="300">
<Grid>
<StackPanel>
<WrapPanel>
<TextBlock Text="ID:" Width="50"/>
<TextBox Text="{Binding EmployeeId}" Width="100"/>
<Button Content="Find" HorizontalAlignment="Left"
Command="{Binding FindCommand}" Name="btnFind"
VerticalAlignment="Top" Width="100" FontWeight="Normal"/>
</WrapPanel>
<TextBlock Name="txbResult">
<Run Text="Employee ID: "/>
<Run Text="{Binding EmployeeFound.Id}" />
<LineBreak/>
<Run Text="Employee Name: "/>
<Run Text="{Binding EmployeeFound.FirstName}" />
<Run Text="{Binding EmployeeFound.LastName}" />
</TextBlock>
</StackPanel>
</Grid>
</Window>
Listing 11-8. The EmployeeFindViewModel Class
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Windows.Input;
using Robusta.TalentManager.WebApi.Client.WinApp.Commands;
using Robusta.TalentManager.WebApi.Dto;
public class EmployeeFindViewModel : ViewModelBase
{
private int employeeId;
private EmployeeDto employeeFound;
public EmployeeFindViewModel()
{
this.FindCommand = new RelayCommand(p => FindEmployee());
}
public ICommand FindCommand { get; private set; }
public int EmployeeId
{
get
{
return employeeId;
}
set
{
employeeId = value;
RaisePropertyChanged(() => this.EmployeeId);
}
}
public EmployeeDto EmployeeFound
{
get
{
return employeeFound;
}
set
{
employeeFound = value;
RaisePropertyChanged(() => this.EmployeeFound);
}
}
private void FindEmployee()
{
HttpClient client = new HttpClient();
string creds = String.Format("{0}:{1}", "jqhuman", "p@ssw0rd!");
byte[] bytes = Encoding.Default.GetBytes(creds);
var header = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(bytes));
client.DefaultRequestHeaders.Authorization = header;
// GET
HttpResponseMessage response = client
.GetAsync(" http://localhost/TalentManager/api/employees/ " + this.EmployeeId)
. Result;
if (response.IsSuccessStatusCode)
{
this.EmployeeFound = response.Content.ReadAsAsync<EmployeeDto>().Result;
}
}
}
Listing 11-9. The Startup Method
using System.Windows;
using Robusta.TalentManager.WebApi.Client.WinApp.ViewModels;
using Robusta.TalentManager.WebApi.Client.WinApp.Views;
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
EmployeeFindViewModel viewModel = new EmployeeFindViewModel();
EmployeeFind view = new EmployeeFind();
view.DataContext = viewModel;
view.Show();
}
}
Listing 11-10. Adding Delay in the Get Method of EmployeesController
[Authorize]
public HttpResponseMessage Get(int id)
{
var employee = repository.Find(id);
if (employee == null)
{
var response = Request.CreateResponse(HttpStatusCode.NotFound, "Employee not found");
throw new HttpResponseException(response);
}
System.Threading.Thread.Sleep(5000);
return Request.CreateResponse<EmployeeDto>(
HttpStatusCode.OK,
mapper.Map<Employee, EmployeeDto>(employee));
}
Listing 11-11. Asynchronous FindEmployee
private async TaskFindEmployee()
{
HttpClient client = new HttpClient();
string creds = String.Format("{0}:{1}", "jqhuman", "p@ssw0rd!");
byte[] bytes = Encoding.Default.GetBytes(creds);
var header = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(bytes));
client.DefaultRequestHeaders.Authorization = header;
// GET
//HttpResponseMessage response = client
// .GetAsync(" http://localhost/TalentManager/api/employees/ " + this.EmployeeId)
// .Result;
HttpResponseMessage response = await client
.GetAsync(
" http://localhost/TalentManager/api/employees/ "
+ this.EmployeeId); // Not calling .Result now
if (response.IsSuccessStatusCode)
{
this.EmployeeFound = response.Content.ReadAsAsync<EmployeeDto>().Result;
}
}
System.Threading.Thread.Sleep(5000);
11.3 Calling a Web API from JavaScript
In this exercise, you will call your web API from JavaScript. ASP.NET Web API is a great technology choice to produce the JSON for your JavaScript application needs. Consuming a web API from JavaScript is a different process than in the preceding exercises, where you consumed the web API from .NET clients. Of course, you will use a JavaScript library such as jQuery (which we use in this exercise), but JavaScript runs under the context of a browser and there are limitations associated with this scenario.
In Chapter 3, we looked at the same-origin policy, which allows JavaScript running on a web page originating from a site (defined by a combination of scheme, hostname, and port number) to access the methods and properties of another page originating from the same site but prevents access to pages originating from different sites. For example, the URI for an employee resource that we have been using all along in this chapter is http://localhost/TalentManager/api/employees/12345. If you try to access this URI from the JavaScript running in a page from another ASP.NET MVC application, say http://localhost:14126/Home/Index, the browser will not allow the call. In Chapter 3, we worked around this problem by using JSONP, which is nothing more than a hack. JSONP will not work with HTTP methods other than GET, such as POST or PUT. A better alternative is Cross-Origin Resource Sharing (CORS), which I’ll describe in this exercise.
The Web Applications Working Group within the W3C has proposed the Cross-Origin Resource Sharing (CORS) recommendation, which provides a way for the browser and the server to work together to determine whether to allow the cross-origin request through the use of HTTP headers. There are two types of cross-site requests :
Another aspect worthy of careful consideration is the security aspect of authentication, specifically how to present the credentials to the web API from JavaScript. The following are the possible scenarios .
In this exercise, you will implement a solution along the lines of the second scenario. You will use the broker that we created in Chapter 10 to obtain the token. JavaScript will POST the credentials to the broker and get back a JWT that it sends to the web API as credential. From the CORS perspective, the call to the broker will be a simple cross-site request, and the call to web API will be a pre-flighted request. For the simple request, we send the Access-Control-Allow-Origin header by manually adding it to the response. For the pre-flighted request, we use the library Thinktecture.IdentityModel.
Note Thinktecture.IdentityModel supports a rich configuration API to control the cross-site access. The CORS implementation of Thinktecture.IdentityModel will be part of the System.Web.Cors namespace in ASP.NET Web API VNext. At the time of writing, this is available only in the nightly builds and hence we use Thinktecture.IdentityModel. By the time you read this chapter, if the System.Web.Cors namespace is available in the stable release, you should make use of it.
public ActionResult Index() { return View(); }
Listing 11-12. The /Home/Index View
@section scripts{
<script type="text/javascript">
$(function () {
$("#login-dialog").dialog({
modal: true,
buttons: {
Ok: function () {
$(this).dialog("close");
$.post(" http://localhost: <brokerport>/handler/jwt ",
{
username: $('#username').val(),
password: $('#password').val()
})
.done(function (token) {
$('#username').val(null);
$('#password').val(null);
$.ajax({
type: 'GET',
url: ' http://localhost/talentmanager/api/employees/1 ',
headers: { 'Authorization': 'Bearer ' + token },
success: function (employee) {
var content = employee.Id + ' ' + employee.FirstName;
content = content + ' ' + employee.LastName;
$('#employee').append($('<li/>', { text: content }));
}
});
});
}
}
});
});
</script>
}
<div>
<div>
<ul id="employee" />
</div>
</div>
<div id="login-dialog" title="Please Login">
<table>
<tr>
<td>User ID:</td>
<td>@Html.TextBox("username")</td>
</tr>
<tr>
<td>Password:</td>
<td>@Html.Password("password")</td>
</tr>
</table>
</div>
Listing 11-13. The Master Layout
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
@Styles.Render("∼/Content/css")
@Styles.Render("∼/Content/themes/base/css")
@Scripts.Render("∼/bundles/modernizr")
</head>
<body>
@RenderBody()
@Scripts.Render("∼/bundles/jquery")
@Scripts.Render("∼/bundles/jqueryui")
@RenderSection("scripts", required: false)
</body>
</html>
Listing 11-14. The CorsConfig Class
using System.Web.Http;
using Thinktecture.IdentityModel.Http.Cors.WebApi;
public static class CorsConfig
{
public static void RegisterCors(HttpConfiguration httpConfig)
{
WebApiCorsConfiguration corsConfig = new WebApiCorsConfiguration();
corsConfig.RegisterGlobal(httpConfig);
corsConfig
.ForResources("Employees")
.ForOrigins(" http://localhost: 14126")
.AllowRequestHeaders("Authorization")
.AllowMethods("GET");
}
}
Listing 11-15. Registering CORS Configuration
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
IocConfig.RegisterDependencyResolver(GlobalConfiguration.Configuration);
WebApiConfig.Register(GlobalConfiguration.Configuration);
DtoMapperConfig.CreateMaps();
CorsConfig.RegisterCors(GlobalConfiguration.Configuration);
}
}
Listing 11-16. BrokerHandler Supporting CORS
using System;
using System.Collections.Generic;
using System.IdentityModel.Protocols.WSTrust;
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Web;
public class BrokerHandler : IHttpHandler
{
private const string ISSUER = "Robusta.Broker";
private const string AUDIENCE = " http://localhost/talentmanager/api ";
private ISet<string> allowedOrigins = new HashSet<string>()
{ " http://localhost:14126 " };
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
HttpRequest request = context.Request;
string userName = request["username"];
string password = request["password"];
bool isAuthentic = !String.IsNullOrEmpty(userName) &&
userName.Equals(password);
if (isAuthentic)
{
// I use a hard-coded key
byte[] key = Convert.FromBase64String(
"qqO5yXcbijtAdYmS2Otyzeze2XQedqy+Tp37wQ3sgTQ=");
var signingCredentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature,
SecurityAlgorithms.Sha256Digest);
var descriptor = new SecurityTokenDescriptor()
{
TokenIssuerName = ISSUER,
AppliesToAddress = AUDIENCE,
Lifetime = new Lifetime(DateTime.UtcNow, DateTime.UtcNow.AddMinutes(5)),
SigningCredentials = signingCredentials,
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role, "HRManager")
})
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(descriptor);
var origin = context.Request.Headers["Origin"];
if(origin != null&& allowedOrigins.Contains(origin))
context.Response.Headers.Add("Access-Control-Allow-Origin", origin);
context.Response.Write(tokenHandler.WriteToken(token));
}
else
context.Response.StatusCode = 401;
}
}
Figure 11-2. Running with Google Chrome
Figure 11-3. The jQuery UI popup
Summary
A broad range of clients in disparate platforms—any software program or hardware device capable of making HTTP calls—can consume HTTP services built using ASP.NET Web API. Consuming your HTTP services from a .NET client is easier with HttpClient, the modern HTTP client that is available from the .NET framework version 4.5 (ported to .NET 4.0 as well). With HttpClient, you get the async benefit of .NET 4.5 by default, and it is quite easy to write non-blocking, responsive applications.
When it comes to JavaScript clients, you need to consider the unique aspects related to the client application running in the context of a browser. There could be restrictions related to the same-origin policy imposed by browsers. CORS is one way to overcome this limitation. The security requirements are also unique with JavaScript applications.