Though ASP.NET Web API has ASP.NET in the name, it is not tied to ASP.NET. In fact, ASP.NET Web API is host-independent. There are three ways you can host your HTTP services built using ASP.NET Web API:
9.1 Web Hosting ASP.NET Web API
In this exercise, you will web-host your ASP.NET Web API in the local IIS server. You will create a new Visual Studio solution with all the related projects in such a way that this solution structure is as host-agnostic as possible. Table 9-1 lists the name of the projects you will create in this exercise with a brief description of the project contents.
Table 9-1. Project Description
Project Name | Description |
---|---|
Robusta.TalentManager.Domain | Contains the domain classes implementing the business rules pertaining to the domain. However, in this exercise, there are not a lot of business rules implemented, since the focus of this book is on ASP.NET Web API and not on solving a domain problem. |
Robusta.TalentManager.Data | Contains the classes related to data access, specifically the classes related to the Entity Framework. |
Robusta.TalentManager.WebApi.Core | Contains the core classes related to ASP.NET Web API, such as controllers, filters, message handlers, configuration files, and so on. |
Robusta.TalentManager.WebApi.Dto | Contains the data transfer object class: the class that is formatted into a response and bound from a request. For this exercise, we will create just one DTO class. |
Robusta.TalentManager.WebApi.WebHost | Contains just the Global.asax and Web.config file, and this is pretty much an empty web application that contains references to the other projects. This is the project that will be deployed in IIS for web hosting. |
This is not the only way you can organize your projects to web-host your HTTP services based on ASP.NET Web API. You can equally well create an ASP.NET MVC 4 project based on the Web API template, as you have been doing so far. I use a slightly different organization here to demonstrate that there are various ways to create your ASP.NET Web API project. Also, by following this approach, we can reuse the projects for self-hosting.
Note For NuGet packages, right-click References under the respective project and select Manage NuGet Packages. Search for and select the package, and click Install.
We will incorporate some of the features we implemented in Chapter 7. There will be some code repetition, but the objective is to get a working application on top of which we will add more functionality, as we progress through the exercises. We will reuse the same database, talent_manager.
Listing 9-1. The Employee Class and the Related Interfaces
public interface IIdentifiable
{
int Id { get; }
}
public interface IVersionable
{
byte[] RowVersion { get; set; }
}
public class Employee : IIdentifiable, IVersionable
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int DepartmentId { get; set; }
public byte[] RowVersion { get; set; }
}
Listing 9-2. The EmployeeConfiguration Class
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using Robusta.TalentManager.Domain;
public class EmployeeConfiguration : EntityTypeConfiguration<Employee>
{
public EmployeeConfiguration()
{
HasKey(k => k.Id);
Property(p => p.Id)
.HasColumnName("employee_id")
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(p => p.FirstName).HasColumnName("first_name");
Property(p => p.LastName).HasColumnName("last_name");
Property(p => p.DepartmentId).HasColumnName("department_id");
Property(p => p.RowVersion).HasColumnName("row_version").IsRowVersion();
}
}
Listing 9-3. The IContext, IRepository, and IUnitOfWork Interfaces
using System;
using System.Linq;
using System.Linq.Expressions;
using Robusta.TalentManager.Domain;
public interface IContext : IDisposable
{
int SaveChanges();
}
public interface IRepository<T> : IDisposable where T : class, IIdentifiable
{
IQueryable<T> All { get; }
IQueryable<T> AllEager(params Expression<Func<T, object>>[] includes);
T Find(int id);
void Insert(T entity);
void Update(T entity);
void Delete(int id);
}
public interface IUnitOfWork : IDisposable
{
int Save();
IContext Context { get; }
}
Listing 9-4. The Context, Repository<T>, and UnitOfWork Concrete Classes
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
using Robusta.TalentManager.Data.Configuration;
public class Context : DbContext, IContext
{
static Context()
{
Database.SetInitializer<Context>(null);
}
public Context() : base("DefaultConnection") { }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Configurations
.Add(new EmployeeConfiguration());
}
}
using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using Robusta.TalentManager.Domain;
public class Repository<T> : IRepository<T> where T : class, IIdentifiable
{
private readonly Context context;
public Repository(IUnitOfWork uow)
{
context = uow.Context as Context;
}
public IQueryable<T> All
{
get
{
return context.Set<T>();
}
}
public IQueryable<T> AllEager(params Expression<Func<T, object>>[] includes)
{
IQueryable<T> query = context.Set<T>();
foreach (var include in includes)
{
query = query.Include(include);
}
return query;
}
public T Find(int id)
{
return context.Set<T>().Find(id);
}
public void Insert(T item)
{
context.Entry(item).State = EntityState.Added;
}
public void Update(T item)
{
context.Set<T>().Attach(item);
context.Entry(item).State = EntityState.Modified;
}
public void Delete(int id)
{
var item = context.Set<T>().Find(id);
context.Set<T>().Remove(item);
}
public void Dispose()
{
context.Dispose();
}
}
public class UnitOfWork : IUnitOfWork
{
private readonly IContext context;
public UnitOfWork()
{
context = new Context();
}
public UnitOfWork(IContext context)
{
this.context = context;
}
public int Save()
{
return context.SaveChanges();
}
public IContext Context
{
get
{
return context;
}
}
public void Dispose()
{
context.Dispose();
}
}
Listing 9-5. The EmployeeDto Class
public class EmployeeDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int DepartmentId { get; set; }
public byte[] RowVersion { get; set; }
}
Listing 9-6. StructureMapContainer and StructureMapDependencyScope
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http.Dependencies;
using StructureMap;
namespace Robusta.TalentManager.WebApi.Core.Infrastructure
{
public class StructureMapDependencyScope : IDependencyScope
{
private readonly IContainer container = null;
public StructureMapDependencyScope(IContainer container)
{
this.container = container;
}
public object GetService(Type serviceType)
{
bool isConcrete = !serviceType.IsAbstract && !serviceType.IsInterface;
return isConcrete ?
container.GetInstance(serviceType):
container.TryGetInstance(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
return container.GetAllInstances<object>()
.Where(s => s.GetType() == serviceType);
}
public void Dispose()
{
if (container != null)
container.Dispose();
}
}
}
using System.Web.Http.Dependencies;
using StructureMap;
namespace Robusta.TalentManager.WebApi.Core.Infrastructure
{
public class StructureMapContainer : StructureMapDependencyScope, IDependencyResolver
{
private readonly IContainer container = null;
public StructureMapContainer(IContainer container)
: base(container)
{
this.container = container;
}
public IDependencyScope BeginScope()
{
return new StructureMapDependencyScope(container.GetNestedContainer());
}
}
}
Listing 9-7. The WebApiConfig, DtoMapperConfig, and IocConfig Classes
using System.Web.Http;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
using System;
using System.Linq;
using System.Web.Http;
using AutoMapper;
using Robusta.TalentManager.Data;
using Robusta.TalentManager.WebApi.Core.Infrastructure;
using StructureMap;
public static class IocConfig
{
public static void RegisterDependencyResolver(HttpConfiguration config)
{
ObjectFactory.Initialize(x =>
{
x.Scan(scan =>
{
scan.WithDefaultConventions();
AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name.StartsWith("Robusta.TalentManager"))
.ToList()
.ForEach(a => scan.Assembly(a));
});
x.For<IMappingEngine>().Use(Mapper.Engine);
x.For(typeof(IRepository<>)).Use(typeof(Repository<>));
});
config.DependencyResolver = new StructureMapContainer(ObjectFactory.Container);
}
}
using AutoMapper;
using Robusta.TalentManager.Domain;
using Robusta.TalentManager.WebApi.Dto;
public static class DtoMapperConfig
{
public static void CreateMaps()
{
Mapper.CreateMap<EmployeeDto, Employee>();
Mapper.CreateMap<Employee, EmployeeDto>();
}
}
Listing 9-8. Global.asax.cs
using System;
using System.Web.Http;
using Robusta.TalentManager.WebApi.Core.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();
}
}
Listing 9-9. Web.Config
<configuration>
<configSections>
<section name="entityFramework"
type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection,
EntityFramework, Version=5.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
requirePermission="false" />
</configSections>
<connectionStrings>
<add name="DefaultConnection"
providerName="System.Data.SqlClient"
connectionString="Server= server;Database=talent_manager;
User Id= uid;Password= pwd;" />
</connectionStrings>
...
</configuration>
Listing 9-10. EmployeesController
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using AutoMapper;
using Robusta.TalentManager.Data;
using Robusta.TalentManager.Domain;
using Robusta.TalentManager.WebApi.Dto;
public class EmployeesController : ApiController
{
private readonly IUnitOfWork uow = null;
private readonly IRepository<Employee> repository = null;
private readonly IMappingEngine mapper = null;
public EmployeesController(IUnitOfWork uow,
IRepository<Employee> repository,
IMappingEngine mapper)
{
this.uow = uow;
this.repository = repository;
this.mapper = mapper;
}
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);
}
return Request.CreateResponse<EmployeeDto>(
HttpStatusCode.OK,
mapper.Map<Employee, EmployeeDto>(employee));
}
public HttpResponseMessage Post(EmployeeDto employeeDto)
{
var employee = mapper.Map<EmployeeDto, Employee>(employeeDto);
repository.Insert(employee);
uow.Save();
var response = Request.CreateResponse<Employee>(
HttpStatusCode.Created,
employee);
string uri = Url.Link("DefaultApi", new { id = employee.Id });
response.Headers.Location = new Uri(uri);
return response;
}
protected override void Dispose(bool disposing)
{
if (repository != null)
repository.Dispose();
if (uow != null)
uow.Dispose();
base.Dispose(disposing);
}
}
It works and returns the JSON representation of the employee resource. Thus, we have created the Visual Studio solution with all the projects from scratch without using the Web API template. The project Robusta.TalentManager.WebApi.WebHost is what you will deploy to IIS to web-host the Web API. You will follow the same process you typically follow to deploy any ASP.NET application, and we will now use local IIS to run our application.
Note If you do not have IIS installed in your computer, the checkbox Use IIS Express will be checked and disabled, and you will not be able to uncheck it. You can install IIS or leave the checkbox checked and use IIS Express, which comes with Visual Studio.
I use IIS 7.5 and depending on the version, what you see in your machine could be different from the screenshots (See Figure 9-1).
Figure 9-1. IIS Manager—Server Certificate Generation
Figure 9-2. IIS Manager—configuring HTTPS binding
Thus, we have hosted our Web API in IIS and enabled HTTPS as well.
Note A self-signed certificate like the one we just created using IIS Manager is signed by the same entity for whose identity it stands. This is similar to you certifying yourself. In the real world, unless you are someone whom everyone else trusts, no one is going to believe the certificate you give to yourself. A third party that is trusted by both the first and second party is needed to complete the circle of trust. In the world of digital certificates, that trusted third party is a certification authority (CA) such as VeriSign. A certificate issued by a CA is trusted by all, but it does cost money. A self-signed certificate costs nothing but is trusted by no one. It can be used for testing purposes only.
9.2 Self-Hosting ASP.NET Web API
In this exercise, you will self-host your ASP.NET Web API. One of the great features of ASP.NET Web API is that it is host-independent. The same code we web-hosted in the previous exercise can be self-hosted. Self-hosting does not require IIS, and you can self-host a Web API in your own process. You will use a console application to self-host Web API in this exercise.
Listing 9-11. The Program Class
using System;
using System.Web.Http.SelfHost;
using Robusta.TalentManager.WebApi.Core.Configuration;
class Program
{
static void Main(string[] args)
{
var configuration = new HttpSelfHostConfiguration(" http://localhost: 8086");
WebApiConfig.Register(configuration);
DtoMapperConfig.CreateMaps();
IocConfig.RegisterDependencyResolver(configuration);
using (HttpSelfHostServer server = new HttpSelfHostServer(configuration))
{
server.OpenAsync().Wait();
Console.WriteLine("Press Enter to terminate the server...");
Console.ReadLine();
}
}
}
Listing 9-12. App.Config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="entityFramework"
type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection,
EntityFramework, Version=5.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<connectionStrings>
<add name="DefaultConnection"
providerName="System.Data.SqlClient"
connectionString="Server= server;Database=talent_manager;
User Id= uid;Password= pwd;" />
</connectionStrings>
</configuration>
We will now enable HTTPS. For this, we will use the same MyWebApiCert server certificate that we generated in Exercise 9.1.
a7 8f f5 7f 70 ce 4f a2 b1 07 41 0f b5 33 79 37 d2 d5 11 67
netsh http add sslcert ipport=0.0.0.0: 8086certhash=a78ff57f70ce4fa2b107410fb5337937d2d51167 appid={951B215B-DE1E-42AD-B82C-4F966867CE41}
Listing 9-13. The MySelfHostConfiguration Class
using System.ServiceModel.Channels;
using System.Web.Http.SelfHost;
using System.Web.Http.SelfHost.Channels;
public class MySelfHostConfiguration : HttpSelfHostConfiguration
{
public MySelfHostConfiguration(string baseAddress) : base(baseAddress) { }
protected override BindingParameterCollection OnConfigureBinding(HttpBinding httpBinding)
{
httpBinding.Security.Mode = HttpBindingSecurityMode.Transport;
return base.OnConfigureBinding(httpBinding);
}
}
var configuration = new HttpSelfHostConfiguration(" http://localhost:8086 ");
and add the line
var configuration = new MySelfHostConfiguration(" https://localhost:8086 ");
As in the case of web hosting, Internet Explorer will not be happy with the self-signed certificate and will show the warning that the certificate is not something it trusts.
Note Our console application listens on port 8086 for HTTP traffic. This requires administrator privileges. This is the reason we run Visual Studio as administrator. You will not be able to run in the elevated level all the time, and it is not a good practice, anyway. But if you do not run as administrator, you will get the error HTTP could not register URL http://+:8086/. To resolve this problem, use Netsh.exe to reserve the URL for non-administrator accounts. Open a command prompt as administrator and run the following command: netsh http add urlacl url=http://+:8086/ user=<your account>. You can delete the URL reservation for non-administrator accounts by running the following command: netsh http delete urlacl url=http://+:8086/.
9.3 In-Memory Hosting ASP.NET Web API
In this exercise, you will use in-memory hosting to integration-test your ASP.NET Web API. In the case of in-memory hosting, nothing goes over the wire. Hence it does not require a port or an IP address, and everything runs in memory. This attribute makes in-memory hosting very useful for testing the ASP.NET Web API pipeline. In Chapter 7, we unit-tested controllers in isolation. There are scenarios where you will want to test the controller along with some other components running in the pipeline, say a filter and a message handler. In-memory hosting makes it possible to test those cases quickly at the same pace that an isolated unit test runs.
Listing 9-14. The GET Action Method with the Authorize Filter
[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);
}
return Request.CreateResponse<EmployeeDto>(
HttpStatusCode.OK,
mapper.Map<Employee, EmployeeDto>(employee));
}
The Authorize filter is not allowing the action method to run, since the user identity is not authenticated. I cover security in Chapter 10, but for the sake of this exercise, let us create a message handler that simply sets a hard-coded but authenticated identity, as long as the request contains the custom header X-PSK.
Listing 9-15. The Authentication Message Handler
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
public class AuthenticationHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request.Headers.Contains("X-PSK"))
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "jqhuman")
};
var principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "dummy") });
Thread.CurrentPrincipal = principal;
}
return await base.SendAsync(request, cancellationToken);
}
}
config.MessageHandlers.Add(new AuthenticationHandler());
using Robusta.TalentManager.WebApi.Core.Handlers;
Now, let us in-memory host our Web API to integration-test all these three classes together: controller, filter, and message handler.
Listing 9-16. App.Config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="entityFramework"
type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection,
EntityFramework, Version=5.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089"
requirePermission="false" />
</configSections>
<connectionStrings>
<add name="DefaultConnection"
providerName="System.Data.SqlClient"
connectionString="Server= server;Database=talent_manager;
User Id= uid;Password= pwd;" />
</connectionStrings>
</configuration>
Listing 9-17. The EmployeesControllerIntegrationTest Class (Incomplete)
using System;
using System.Net;
using System.Net.Http;
using System.Security.Principal;
using System.Threading;
using System.Web.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Robusta.TalentManager.WebApi.Core.Configuration;
using Robusta.TalentManager.WebApi.Dto;
[TestClass]
public class EmployeesControllerIntegrationTest
{
private HttpServer server = null;
[TestInitialize()]
public void Initialize()
{
var configuration = new HttpConfiguration();
IocConfig.RegisterDependencyResolver(configuration);
WebApiConfig.Register(configuration);
DtoMapperConfig.CreateMaps();
server = new HttpServer(configuration);
// This test runs under the context of my user
// account (Windows Identity) and hence I clear that
Thread.CurrentPrincipal = new GenericPrincipal(
new GenericIdentity(String.Empty),
null);
}
// Test methods go here
[TestCleanup]
public void Cleanup()
{
if (server != null)
server.Dispose();
}
}
Listing 9-18. The MustReturn401WhenNoCredentialsInRequest Test Method
[TestMethod]
public void MustReturn401WhenNoCredentialsInRequest()
{
using (var invoker = new HttpMessageInvoker(server))
{
using (var request = new HttpRequestMessage(HttpMethod.Get,
" http://localhost/api/employees/1 "))
{
using (var response = invoker.SendAsync(request,
CancellationToken.None).Result)
{
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
}
}
Listing 9-19. The MustReturn200AndEmployeeWhenCredentialsAreSupplied Test Method
[TestMethod]
public void MustReturn200AndEmployeeWhenCredentialsAreSupplied()
{
using (var invoker = new HttpMessageInvoker(server))
{
using (var request = new HttpRequestMessage(HttpMethod.Get,
" http://localhost/api/employees/1 "))
{
request.Headers.Add("X-PSK", "somekey"); // Credentials
using (var response = invoker.SendAsync(request,
CancellationToken.None).Result)
{
Assert.IsNotNull(response);
Assert.IsNotNull(response.Content);
Assert.IsInstanceOfType(response.Content, typeof(ObjectContent<EmployeeDto>));
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
var content = (response.Content as ObjectContent<EmployeeDto>);
var result = content.Value as EmployeeDto;
Assert.AreEqual(1, result.Id);
Assert.AreEqual("Johnny", result.FirstName);
Assert.AreEqual("Human", result.LastName);
}
}
}
}
Summary
ASP.NET Web API is host-independent, regardless of what the name suggests. ASP.NET infrastructure with IIS is not mandatory. There are three ways you can host your HTTP services built using ASP.NET Web API: web hosting, self-hosting, and in-memory hosting.
Web hosting uses the ASP.NET infrastructure backed by the IIS, and this is similar to hosting any ASP.NET application in IIS. Setting up transport security (HTTPS) is also similar to the way you set up HTTPS for any other ASP.NET application.
ASP.NET Web API can be self-hosted using any Windows process, such as a console application or Windows service. This option is useful when you do not or cannot have IIS. To enable HTTPS or reserve the URL for your Web API, you will need to use the netsh utility.
In the case of in-memory hosting, the clients connect directly to the Web API runtime, without hitting the network. This is used mainly for integration testing purposes, where you want to test the integration of multiple components running in the ASP.NET Web API pipeline.