Hosting HTTP Resources
Introduction
How do I host thee? Let me count the ways!
You may not have realized that .NET offers a lot of alternatives when it comes to hosting an HTTP server, that is, without resorting to IIS, IIS Express or the now gone Visual Studio Web Development Server (aka, Cassini, rest in peace); by that, I either mean:
- Opening up a TCP port and listening for HTTP requests, or a subset of them;
- Running ASP.NET pages without a server.
In this post I am going through some of them. Some are specific to web services, but since they understand REST, I think they qualify as well as generic HTTP hosting mechanisms.
.NET HttpListener
Let’s start with HttpListener. This is included in .NET since version 2 and offers a decent server for static contents, that is, it cannot run any dynamic contents, like ASP.NET handlers, nor does it know anything about them. You merely point it to a physical folder on your file system, and it will happily serve any contents located inside it. Let’s see an example:
using (var listener = new System.Net.HttpListener())
{
var url = "http://*:2000/";
listener.Prefixes.Add(url);
listener.Start();
var ctx = listener.GetContext();
var message = "Hello, World!";
ctx.Response.StatusCode = (Int32) HttpStatusCode.OK;
ctx.Response.ContentType = "text/plain";
ctx.Response.ContentLength64 = message.Length;
using (var writer = new StreamWriter(ctx.Response.OutputStream))
{
writer.Write(message);
}
Console.ReadLine();
}
ASP.NET ApplicationHost
Complementary to HttpListener, we have a way to execute ASP.NET handlers (ASPX pages, ASHX generic handlers and ASMX web services) in a self-hosted application domain. For that, we use the ApplicationHost class to create the ASP.NET application domain, and a regular .NET class for the server implementation. An example:
public class Host : MarshalByRefObject
{
public void ProcessPage(String page, String query, TextWriter writer)
{
var worker = new SimpleWorkerRequest(page, query, writer);
HttpRuntime.ProcessRequest(worker);
}
}
//strip out bin\debug, so as to find the base path where web files are located
var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).Replace(@"\bin\Debug", String.Empty);
//we need to copy the assembly to the base path
File.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(path, "bin", Assembly.GetExecutingAssembly().CodeBase.Split('/').Last()), true);
var host = System.Web.Hosting.ApplicationHost.CreateApplicationHost(typeof(Host), "/", path) as Host;
host.ProcessPage("Default.aspx", null);
Notice the File.Copy call; this is necessary because the assembly referenced by the Default.aspx page needs to be located in the same folder as the page. An alternative to this would be to add a post build-event to the Visual Studio project:
I leave as an exercise to the interested readers how we can combine this with HttpListener!
OWIN WebApp
Moving on to more recent technologies, we now have OWIN. In case you’ve been living in another world and haven’t heard of OWIN, I’ll just say that it is a standard for decoupling .NET from any particular web servers, like IIS or IIS Express. It also happens to have a self-hosting implementation – which, by the way, uses HttpListener underneath.
We need to add a reference to the Microsoft.Owin.SelfHost NuGet package:
After that, we just register an instance of WebApp with the default parameters, add an handler, and we’re done:
class Program
{
public static void Configuration(IAppBuilder app)
{
app.Use(new Func<AppFunc, AppFunc>(next => (async ctx =>
{
using (var writer = new StreamWriter(ctx["owin.ResponseBody"] as Stream))
{
await writer.WriteAsync("Hello, World!");
}
})));
}
static void Main(String[] args)
{
using (WebApp.Start<Program>("http://*:2000"))
{
Console.ReadLine();
}
}
}
Again, no fancy dynamic stuff, just plain and simple HTTP: it waits for a request and just returns Hello, World!. It is possible to run ASP.NET MVC on top of OWIN, that is the goal of project Helios, which is currently in alpha stage. Do check out the Helios NuGet package at https://www.nuget.org/packages/Microsoft.Owin.Host.IIS/1.0.0-alpha1:
WCF ServiceHost
Since its release, WCF offers a way for it to be self-hosted in a .NET process. The class responsible for that is ServiceHost, or one of its descendants, like WebServiceHost, more suitable for REST. I will show an example using REST, which can be easily tested using a web browser:
[ServiceContract]
public interface IRest
{
[WebGet(ResponseFormat = WebMessageFormat.Json)]
[OperationContract]
String Index();
}
public class Rest : IRest
{
public String Index()
{
return "Hello, World!";
}
}
using (var host = new WebServiceHost(typeof(Rest)))
{
var url = new Uri(@"http://localhost:2000");
var binding = new WebHttpBinding();
host.AddServiceEndpoint(typeof(IRest), binding, url);
host.Open();
Console.ReadLine();
}
This example listens for a request of /Index on port 2000 and upon receiving it, returns Hello, World! in JSON format – because we are only sending a string, it will be wrapped in “. WCF REST out of the box only supports returning data in XML or JSON format, no Text or HTML, but, to be fair, that’s not what it was meant to. Should be possible to return HTML, but, honestly, it would probably mean more work than it's worth.
Web API HttpServer
Another web services technology in the .NET stack is Web API. Web API uses a concept similar to MVC, with controllers, models and action methods, but no views. It can be self-hosted as well, using the HttpServer class. In order to use it, install the Microsoft.AspNet.WebApi.SelfHost NuGet package. You will notice that its description claims that it is legacy, and has been replaced for another based on OWIN, yet, it is fully functional, if you don’t required it to be OWIN-compliant:
Because of the Web API architecture, we need to implement a controller for handling requests, :
public class DummyController : ApiController
{
[HttpGet]
public IHttpActionResult Index()
{
return this.Content(HttpStatusCode.OK, "Hello, World!");
}
}
In this example, we do not take any parameters and just return the usual response.
Here’s the infrastructure code:
var url = "http://localhost:2000";
var config = new HttpSelfHostConfiguration(url);
config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}");
using (var server = new HttpSelfHostServer(config))
{
server.OpenAsync().Wait();
}
The DummyController is found by reflecting the current executing assembly and applying conventions; any HTTP requests for /api/Dummy/Index will land there and the outcome will be plain text.
IIS Hostable Web Core
Now, this one is tricky. IIS, from version 7, allows hosting its core engine in-process, that is, from inside another application; this is called IIS Hostable Web Core (HWC). We can supply our own Web.config and ApplicationHost.config files and specify a root folder from which IIS will serve our web resources, including any dynamic contents that IIS can serve (ASPX pages, ASHX handlers, ASMX and WCF web services, etc). Yes, I know, this contradicts my introduction, where I claimed that this post would be about hosting web resources without IIS... still, I think this is important to know, because it can be fully controlled through code.
You need to make sure HWC is installed... one option is using PowerShell's Install-WindowsFeature cmdlet:
Or the Server Manager application:
Because HWC is controlled through an unmanaged DLL, we have to import its public API control functions and call it with .NET code. Here's an example:
public class Host : IDisposable
{
private static readonly String FrameworkDirectory = RuntimeEnvironment.GetRuntimeDirectory();
private static readonly String RootWebConfigPath = Environment.ExpandEnvironmentVariables(Path.Combine(FrameworkDirectory, @"Config\Web.config"));
public Host(String physicalPath, Int32 port)
{
this.ApplicationHostConfigurationPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName() + ".config");
this.PhysicalPath = physicalPath;
this.Port = port;
var applicationHostConfigurationContent = File.ReadAllText("ApplicationHost.config");
var text = String.Format(applicationHostConfigurationContent, this.PhysicalPath, this.Port);
File.WriteAllText(this.ApplicationHostConfigurationPath, text);
}
~Host()
{
this.Dispose(false);
}
public String ApplicationHostConfigurationPath
{
get;
private set;
}
public Int32 Port
{
get;
private set;
}
public String PhysicalPath
{
get;
private set;
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(Boolean disposing)
{
this.Stop();
}
public void Start()
{
if (IisHostableWebCoreEngine.IsActivated == false)
{
IisHostableWebCoreEngine.Activate(this.ApplicationHostConfigurationPath, RootWebConfigPath, Guid.NewGuid().ToString());
}
}
public void Stop()
{
if (IisHostableWebCoreEngine.IsActivated == true)
{
IisHostableWebCoreEngine.Shutdown(false);
this.PhysicalPath = String.Empty;
this.Port = 0;
File.Delete(this.ApplicationHostConfigurationPath);
this.ApplicationHostConfigurationPath = String.Empty;
}
}
private static class IisHostableWebCoreEngine
{
private delegate Int32 FnWebCoreActivate([In, MarshalAs(UnmanagedType.LPWStr)] String appHostConfig, [In, MarshalAs(UnmanagedType.LPWStr)] String rootWebConfig, [In, MarshalAs(UnmanagedType.LPWStr)] String instanceName);
private delegate Int32 FnWebCoreShutdown(Boolean immediate);
private const String HostableWebCorePath = @"%WinDir%\System32\InetSrv\HWebCore.dll";
private static readonly IntPtr HostableWebCoreLibrary = LoadLibrary(Environment.ExpandEnvironmentVariables(HostableWebCorePath));
private static readonly IntPtr WebCoreActivateAddress = GetProcAddress(HostableWebCoreLibrary, "WebCoreActivate");
private static readonly FnWebCoreActivate WebCoreActivate = Marshal.GetDelegateForFunctionPointer(WebCoreActivateAddress, typeof(FnWebCoreActivate)) as FnWebCoreActivate;
private static readonly IntPtr WebCoreShutdownAddress = GetProcAddress(HostableWebCoreLibrary, "WebCoreShutdown");
private static readonly FnWebCoreShutdown WebCoreShutdown = Marshal.GetDelegateForFunctionPointer(WebCoreShutdownAddress, typeof(FnWebCoreShutdown)) as FnWebCoreShutdown;
internal static Boolean IsActivated
{
get;
private set;
}
internal static void Activate(String appHostConfig, String rootWebConfig, String instanceName)
{
var result = WebCoreActivate(appHostConfig, rootWebConfig, instanceName);
if (result != 0)
{
Marshal.ThrowExceptionForHR(result);
}
IsActivated = true;
}
internal static void Shutdown(Boolean immediate)
{
if (IsActivated == true)
{
WebCoreShutdown(immediate);
IsActivated = false;
}
}
[DllImport("Kernel32.dll")]
private static extern IntPtr LoadLibrary(String dllname);
[DllImport("Kernel32.dll")]
private static extern IntPtr GetProcAddress(IntPtr hModule, String procname);
}
}
In order for this to work, we need to have an ApplicationHost.config file, a minimum working example being:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<configSections>
<sectionGroup name="system.applicationHost">
<section name="applicationPools" />
<section name="sites" />
</sectionGroup>
<sectionGroup name="system.webServer">
<section name="globalModules" />
<section name="modules" />
<section name="handlers" />
<section name="staticContent" />
<section name="serverRuntime" />
<sectionGroup name="security">
<section name="access"/>
<sectionGroup name="authentication">
<section name="anonymousAuthentication" />
<section name="windowsAuthentication" />
<section name="basicAuthentication" />
</sectionGroup>
<section name="authorization" />
<section name="requestFiltering" />
<section name="applicationDependencies" />
<section name="ipSecurity" />
</sectionGroup>
<section name="asp" />
<section name="caching" />
<section name="cgi" />
<section name="defaultDocument" />
<section name="directoryBrowse" />
<section name="httpErrors" />
<section name="httpLogging" />
<section name="httpProtocol" />
<section name="httpRedirect" />
<section name="httpTracing" />
<section name="isapiFilters" allowDefinition="MachineToApplication" />
<section name="odbcLogging" />
</sectionGroup>
</configSections>
<system.applicationHost>
<applicationPools>
<add name="AppPool" managedPipelineMode="Integrated" managedRuntimeVersion="v4.0" autoStart="true" />
</applicationPools>
<sites>
<site name="MySite" id="1">
<bindings>
<binding protocol="http" bindingInformation="*:{1}:localhost" />
</bindings>
<application path="/" applicationPool="AppPool" >
<virtualDirectory path="/" physicalPath="{0}" />
</application>
</site>
</sites>
</system.applicationHost>
<system.webServer>
<globalModules>
<add name="StaticFileModule" image="%windir%\System32\inetsrv\static.dll" />
<add name="AnonymousAuthenticationModule" image="%windir%\System32\inetsrv\authanon.dll" />
<add name="ManagedEngine" image="%windir%\Microsoft.NET\Framework\v4.0.30319\webengine4.dll" />
</globalModules>
<modules>
<add name="StaticFileModule" />
<add name="AnonymousAuthenticationModule" />
<add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" preCondition="managedHandler" />
<add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" preCondition="managedHandler" />
<add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" preCondition="managedHandler" />
<add name="AnonymousIdentification" type="System.Web.Security.AnonymousIdentificationModule" preCondition="managedHandler" />
</modules>
<handlers accessPolicy="Read, Script">
<add name="PageHandlerFactory-Integrated" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode" />
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" resourceType="Either" requireAccess="Read" />
</handlers>
<staticContent>
<mimeMap fileExtension=".html" mimeType="text/html" />
<mimeMap fileExtension=".jpg" mimeType="image/jpeg" />
<mimeMap fileExtension=".gif" mimeType="image/gif" />
<mimeMap fileExtension=".png" mimeType="image/png" />
</staticContent>
</system.webServer>
</configuration>
And all we need to start hosting pages on the port and physical path specified by ApplicationHost.config is:
using (var host = new Host(path, port))
{
host.Start();
Console.ReadLine();
}
A couple of notes:
-
Because it calls unmanaged functions, can be terrible to debug;
-
The ApplicationHost.config needs to be in the application's binary build directory and must have two placeholders, {0} and {1}, for the physical path and HTTP port, respectively;
-
It refers to .NET 4.0, if you want to change it, you will to change a number of modules and paths;
-
Only very few modules are loaded, if you want, get a full file from %HOMEPATH%\Documents\IISExpress\config\ApplicationHost.config and adapt it to your likings.
.NET TcpListener
And finally, one for the low-level guys. The TcpListener class allows the opening of TCP/IP ports and the handling of requests coming through them. It doesn’t know anything about the HTTP protocol, of course, so, if we want to leverage it, we need to implement it ourselves. Here’s a very, very, basic example:
var listener = System.Net.Sockets.TcpListener.Create(2000);
listener.Start();
using (var client = listener.AcceptTcpClient())
{
using (var reader = new StreamReader(client.GetStream()))
using (var writer = new StreamWriter(client.GetStream()))
{
var request = reader.ReadLine();
writer.WriteLine("HTTP/1.1 200 OK");
writer.WriteLine("Content-type: text/plain");
writer.WriteLine();
writer.WriteLine("Hello, World!");
writer.Flush();
}
}
listener.Stop();
Here we’re just reading any string content and responding with some HTTP headers plus the usual response. Of course, HTTP is quite complex, so I wouldn’t recommend you try to implement it yourself.
Conclusion
I presented a couple of solutions for hosting web resources, servicing HTTP requests or running ASP.NET handlers. Hopefully you will find one that matches your needs.