Service Station

Run ASMX Without IIS

Aaron Skonnard

Code download available at:ServiceStation0412.exe(165 KB)


ASP.NET HTTP Architecture
Hosting the HTTP Pipeline
Move Over, IIS
HttpListener: Implementing Your Own Web Server
Hosting ASMX
Hosting Models: Web Services on the Desktop
Where Are We?

When the Microsoft® .NET Framework first shipped, it introduced a breakthrough Web services framework known as ASMX. The motivation behind the ASMX design was to simplify the process of developing Web services as much as possible so that even if you're not an XML expert, you can get a Web service up and running. ASMX accomplished this by hiding most of the underlying XML and Web services details. Instead of forcing developers to deal directly with SOAP envelopes and Web Services Description Language (WSDL) files, ASMX introduced automatic mapping layers that bridge the gap with traditional .NET code.

ASMX is also tightly integrated with the popular ASP.NET HTTP pipeline. Hence, it enjoys the same benefits as traditional ASP.NET Web applications, such as a sophisticated hosting environment and process model, robust configuration and deployment options, and flexible extensibility points. As a result, ASMX is usually the first stop for most Web services developers. Most developers incorrectly assume that ASMX requires IIS; after all, it's the only use case they've ever seen. But the truth is that ASMX doesn't have any technical dependencies on IIS whatsoever.

The need to host Web services without IIS is very real. In some environments, it may not be possible to have IIS running on the machine that must host the Web service for a variety of reasons. Luckily, you can host ASMX in your own process without IIS. It's been possible to do this since the release of the .NET Framework 1.0, but you had to provide your own Web server to receive HTTP requests. Cassini is a sample Web server developed by the ASP.NET team that filled this need and allowed you to run ASP pages without IIS. However, writing your own Web server, or using a sample like Cassini, does not seem reasonable to most developers.

Since the release of Windows Server™ 2003 and Windows® XP SP2, there has been a new HTTP protocol stack available called http.sys. With http.sys and some new managed classes in the .NET Framework 2.0 (especially HttpListener), you can easily build a Web server right into your applications without requiring IIS on the machine. These advances have made it possible to run ASMX everywhere. Keep in mind that the .NET Framework 2.0 is currently in Beta and is therefore subject to change.

ASP.NET HTTP Architecture

ASP.NET was specifically designed to avoid dependencies on IIS. The underlying architecture consists of a pipeline of .NET classes that work together to process incoming HTTP messages. It's considered a pipeline because each HTTP request travels through a sequence of objects, each performing some processing.

The HttpRuntime class sits at the beginning of the pipeline and is responsible for starting the process. The pipeline begins executing when the static ProcessRequest method is called on the HttpRuntime class. ProcessRequest takes an HttpWorkerRequest object, which contains all the information in the current request. HttpRuntime uses the information in the HttpWorkerRequest to populate the HttpContext object. It then instantiates the appropriate HttpApplication class, which will invoke any IHttpModule implementations registered with the application for pre/post-processing. At this point, the appropriate IHttpHandler implementation is identified, instantiated, and invoked.

This process occurs for each HTTP request that enters the pipeline. All of the ASP.NET functionality (including that of ASMX) is contained in the pipeline classes. For example, support for processing ASMX endpoints begins when the request reaches the System.Web.Services.Protocols.WebServiceHandlerFactory class, which is responsible for identifying, compiling (if necessary), and instantiating the identified ASMX class, as well as invoking the WebMethod targeted by the incoming SOAP message.

Figure 1 HTTP Pipeline and Web Servers

Figure 1** HTTP Pipeline and Web Servers **

The pipeline is completely autonomous and disconnected from IIS. It even runs in a separate process from inetinfo.exe when used in conjunction with IIS. The name of the process depends on the host OS (aspnet_wp.exe on Windows XP, w3wp.exe on Windows Server 2003). In addition to having its own process model, the pipeline also has an independent configuration story that is decoupled from the IIS metabase. The only thing the pipeline doesn't come with is its own Web server that's capable of receiving incoming HTTP requests. You still need something like IIS 5.0 or http.sys that is capable of listening for incoming HTTP messages. Even then, these components are only responsible for receiving the HTTP requests and handing them off to the ASP.NET pipeline, which handles everything from that point (see Figure 1).

Once the request makes it into the worker process, the worker process creates an HttpWorkerRequest object (representing the incoming request) and calls HttpRuntime.ProcessRequest to launch the pipeline. Because of this elegant design, it is possible to call HttpRuntime directly within your own app.

Hosting the HTTP Pipeline

The classes you need to host ASP.NET are found in the System.Web and System.Web.Hosting namespaces. The main classes you'll need to get started are ApplicationHost, HttpRuntime, and an HttpWorkerRequest-derived class. You start by calling ApplicationHost.CreateApplicationHost. This method creates a new application domain (AppDomain) capable of processing ASP.NET requests. Since you're explicitly creating the AppDomain, you must specify the virtual directory and corresponding physical directory when making the call.

In addition to creating the new AppDomain, CreateApplicationHost also instantiates an object within the new AppDomain through which you can communicate. You specify the type you would like it to instantiate when making the method call. Since the object will be used across AppDomain boundaries, it must derive from MarshalByRefObject. You'll want to use your own class that has the methods you need to interact with the AppDomain. For example, at a minimum you'll want a ProcessRequest method that makes it possible to submit a new ASP.NET request for processing.

Here's a class that could be used for this purpose:

public class MySimpleHost : MarshalByRefObject { public void ProcessRequest(string file) { ... // use the ASP.NET HTTP pipeline to process request } }

In this case, ProcessRequest accepts the file name of the page to be processed. Within ProcessRequest, you can use HttpRuntime to start the pipeline processing. HttpRuntime has a static method, also named ProcessRequest, which takes an argument of type HttpWorkerRequest.

HttpWorkerRequest is an abstract class, but luckily .NET ships a simple derived class called SimpleWorkerRequest, which was designed to handle simple HTTP GET requests. When you instantiate SimpleWorkerRequest, you must specify the name of the page to be processed, an optional query string, and a TextWriter to which the pipeline can write the output. Once you have an HttpWorkerRequest object, you invoke the pipeline by calling ProcessRequest, as illustrated here:

... // MySimpleHost.ProcessRequest SimpleWorkerRequest swr = new SimpleWorkerRequest(page, null, Console.Out); HttpRuntime.ProcessRequest(swr);

With MySimpleHost in place, you need to call ApplicationHost.CreateApplicationHost from within the host application to instantiate the object. Then, you can use MySimpleHost.ProcessRequest to send requests into the HTTP pipeline for processing, as shown in the following code snippet:

... // console host application MySimpleHost msh = (MySimpleHost) ApplicationHost.CreateApplicationHost(typeof(MySimpleHost), "/", Directory.GetCurrentDirectory()); foreach (string page in args) msh.ProcessRequest(page);

The implementation of ApplicationHost.CreateApplicationHost expects to find the assembly of the specified type in one of two places: either in the Global Assembly Cache (GAC) or within the bin directory under the specified physical directory. There is no documented way to change this behavior short of reimplementing CreateApplicationHost. So depending on your project configuration and deployment scenario, you may need to install the assembly into one of these locations.

Figure 2 contains the code for a complete console application that hosts ASP.NET. This sample is available for download. You specify the name of the ASP.NET file along with an optional query string on the command line. Then the program passes them into the pipeline through the call to MySimpleHost.ProcessRequest.

Figure 2 Simple ASP.NET Host

Using System; using System.Web; using System.Web.Hosting; using System.IO; ... class Program { static void Main(string[] args) { if (args.Length == 0) { Console.WriteLine( "Usage: simplehost filename [querystring]"); return; } string file = args[0]; string query = (args.Length > 1) ? args[1] : ""; MySimpleHost msh = (MySimpleHost) ApplicationHost.CreateApplicationHost( typeof(MySimpleHost), "/", Directory.GetCurrentDirectory()); msh.ProcessRequest(file, query); } } public class MySimpleHost : MarshalByRefObject { public void ProcessRequest(string file, string query) { SimpleWorkerRequest swr = new SimpleWorkerRequest(file, query, Console.Out); HttpRuntime.ProcessRequest(swr); } }

In the download for this column, I've provided a few ASP.NET files that you can use to experiment, including one called math.asmx. When you run the app, specifying "math.asmx WSDL" on the command line, you'll see the ASMX-generated WSDL definition printed to the console window (equivalent to browsing to https://host/math.asmx?WSDL with math.asmx hosted in IIS). If you specify only "math.asmx" on the command line, it will print the ASMX-generated human-readable documentation page.

This obviously isn't a realistic example since you have to specify the ASP.NET page on the command line. In the real world this information would come in through an HTTP request. In order to support HTTP, you need to integrate a Web server with your app.

Move Over, IIS

Http.sys is a new low-level HTTP protocol stack available in Windows Server 2003 and Windows XP SP2. Http.sys is a kernel-mode component that offers its HTTP services to all applications on the machine. This means HTTP support is now deeply ingrained into the OS. Even IIS 6.0 has been rearchitected to use http.sys (see Figure 3).

Figure 3 Http.sys Architecture

Figure 3** Http.sys Architecture **

Before version 6.0, IIS relied on the TCP/IP kernel and the Windows Sockets API (Winsock) to receive HTTP requests. Since Winsock is a user-mode component, each receive operation required switching between kernel mode and user mode. Http.sys is now capable of caching responses directly within the kernel. Lowering the HTTP stack into the kernel makes it possible to remove this expensive context switch when serving cached responses, boosting efficiency and overall throughput.

When http.sys receives a request, it can forward it directly to the correct worker process. Also, if the worker process isn't available to accept the request, http.sys stores it until the worker process starts up and is able to get it. This means that worker process failures don't interrupt service. When IIS 6.0 starts up, the WWW Service communicates with http.sys and registers the routing information for each configured IIS application. Whenever you create or remove applications from IIS, the WWW Service communicates with http.sys to update its routing information.

As you can see in Figure 3, http.sys lays the foundation for the IIS 6.0 Web architecture, but it's not tied to IIS in any way. Any application running on the machine can take advantage of http.sys for receiving HTTP requests. Like the WWW Service, you register your application with http.sys and begin listening for incoming HTTP requests. The .NET Framework 2.0 introduces a suite of managed classes that make this very easy to do.

HttpListener: Implementing Your Own Web Server

System.Net contains several new classes for interfacing with http.sys. HttpListener is the focal point of these classes. Using it, you can create a simple Web server (or listener) that responds to incoming HTTP requests. The listener remains active for the lifetime of the HttpListener object, although you can tell it to start and stop listening on command.

In order to begin using HttpListener, you must first instantiate it. Then you indicate which HTTP URLs the listener should process by adding URL prefixes to the Prefixes property. Each URI must contain a scheme ("http" or "https"), a host, an optional port, and an optional path. Each prefix must end with a forward slash:

HttpListener listener = new HttpListener(); listener.Prefixes.Add("https://localhost:8081/foo/"); listener.Prefixes.Add(""); listener.Start();

Only one HttpListener can be listening for a specific URI prefix. You'll get a Win32Exception exception if you attempt to add a duplicate. When you specify a port, you can replace the host name with "*" to indicate that this listener should handle all URIs with this port unless they're matched by another HttpListener. Or you can replace the host name with "+" to indicate that the listener accepts all requests to the specified port, as illustrated here:

HttpListener listener = new HttpListener(); listener.Prefixes.Add("https://+:8081/"); listener.Start();

You can also specify the authentication scheme used by the listener through the AuthenticationScheme property. HttpListener supports anonymous, basic, digest, and Windows authentication. It also supports secure sockets layer (SSL) connections, so you can safely use basic authentication over HTTPS, as you see here:

HttpListener listener = new HttpListener(); listener.AuthenticationSchemes = AuthenticationSchemes.Basic; listener.Prefixes.Add("https://+:8081/"); listener.Start();

Once you've specified the URI prefixes that the listener will process, you must call the Start method as you just saw (note: you must add at least one prefix before calling Start). Start doesn't really do anything noticeable—it simply prepares the listener object to begin receiving requests.

In order to receive a request, you must call GetContext, like this:

HttpListenerContext ctx = listener.GetContext();

GetContext is a synchronous call that blocks while waiting for an incoming request to arrive. It doesn't return until one is received. HttpListener also provides an asynchronous mechanism for receiving a request through BeginGetContext and EndGetContext. GetContext and EndGetContext return an HttpListenerContext object, which represents the received HTTP request.

You can use HttpListener to integrate your own Web server with the sample application shown in Figure 2. All you have to do is forward the requests from the HttpListener to the HTTP pipeline in your code. You can add a loop that continually calls GetContext and uses the information in the returned HttpListenerContext object to invoke ProcessRequest. Then you'll use the HttpListenerContext class to look up the requested file name (using the Request.Url.LocalPath property, like "/math.asmx") and the query string (using the Request.Url.Query property, for example "?WSDL"). With this code, you've made it possible to request ASP.NET pages over HTTP, as shown in Figure 4.

Figure 4 Handling HTTP Requests with HttpListener

MySimpleHost msh = (MySimpleHost) ApplicationHost.CreateApplicationHost( typeof(MySimpleHost), "/", Directory.GetCurrentDirectory()); HttpListener listener = new HttpListener(); listener.Prefixes.Add("https://localhost:8081/"); listener.Prefixes.Add(""); listener.Start(); while (true) { HttpListenerContext ctx = listener.GetContext(); string file = ctx.Request.Url.LocalPath.Replace("/", ""); string query = ctx.Request.Url.Query.Replace("?", ""); msh.ProcessRequest(file, query); }

After making these changes and running the program, the console application will be waiting for GetContext to return. Now you can open a Web browser and navigate to the registered URI (in this example, https://localhost:8081/math.asmx). After doing this, GetContext returns and the request is handed off to the HTTP pipeline. Then you should see the response written to the console window as you did before.

HttpListenerContext is similar to HttpContext in that it provides Request, Response, and User properties that make it easy to interface with all aspects of the HTTP message. The Request property is of type HttpListenerRequest, while the Response property is of type HttpListenerResponse. You use HttpListenerRequest to access the HTTP method, URL, headers, user-agent, body, and other parts of the request. You use HttpListenerResponse to write the HTTP response back to the client. The User property returns an IPrincipal with information about the authenticated user.

You can use these properties to extend the sample further so it's capable of writing directly to the HTTP response stream. You can do this by modifying the signature of MySimpleHost.ProcessRequest to accept a TextWriter. After that, you can wrap the HttpListenerContext.Response.OutputStream in a StreamWriter object and pass it in.

Figure 6 Browser Output

Figure 6** Browser Output **

Figure 5 provides the complete code for this revised sample with an integrated HttpListener. Now when you run the sample, you can use a browser to read and navigate around the ASMX-generated document page (see Figure 6). A message is printed to the console window each time the app receives a request (see Figure 7).

Figure 5 Simple ASP.NET Host Using HttpListener

using System; using System.Web; using System.Web.Hosting; using System.Net; using System.IO; ... class Program { static void Main(string[] args) { MySimpleHost msh = (MySimpleHost) ApplicationHost.CreateApplicationHost( typeof(MySimpleHost), "/", Directory.GetCurrentDirectory()); HttpListener listener = new HttpListener(); listener.Prefixes.Add("https://localhost:8081/"); listener.Prefixes.Add(""); listener.Start(); Console.WriteLine( "Listening for requests on https://localhost:8081/"); while (true) { HttpListenerContext ctx = listener.GetContext(); string page = ctx.Request.Url.LocalPath.Replace("/", ""); string query = ctx.Request.Url.Query.Replace("?", ""); Console.WriteLine("Received request for {0}?{1}", page, query); StreamWriter sw = new StreamWriter(ctx.Response.OutputStream); msh.ProcessRequest(page, query, sw); sw.Flush(); ctx.Response.Close(); } } } public class MySimpleHost : MarshalByRefObject { public void ProcessRequest(string p, string q, TextWriter tw) { SimpleWorkerRequest swr = new SimpleWorkerRequest(p, q, tw); HttpRuntime.ProcessRequest(swr); } }

Figure 7 Console Application Output

Figure 7** Console Application Output **

This sample illustrates a hosting model capable of dealing with HTTP GET requests and query strings. SimpleWorkerRequest was only designed for this simple case, not for more advanced POST operations. Hence, the sample can't fully host ASMX endpoints, which require POST support to process incoming SOAP requests.

Hosting ASMX

To fully support ASMX endpoints, you need a custom HttpWorkerRequest that knows how to deal with the request/response streams. It should be based on the HttpListenerContext object you get back from GetContext. This is a somewhat painful task since the HttpWorkerRequest is fairly large and the documentation isn't completely up to par. As a result, I've provided a sample implementation called HttpListenerWorkerRequest in Figure 8.

Figure 8 HttpListenerWorkerRequest Implementation

public class HttpListenerWorkerRequest : HttpWorkerRequest { private HttpListenerContext _context; private string _virtualDir; private string _physicalDir; public HttpListenerWorkerRequest( HttpListenerContext context, string vdir, string pdir) { if (null == context) throw new ArgumentNullException("context"); if (null == vdir || vdir.Equals("")) throw new ArgumentException("vdir"); if (null == pdir || pdir.Equals("")) throw new ArgumentException("pdir"); _context = context; _virtualDir = vdir; _physicalDir = pdir; } // required overrides (abstract) public override void EndOfRequest() { _context.Response.OutputStream.Close(); _context.Response.Close(); _context.Close(); } public override void FlushResponse(bool finalFlush) { _context.Response.OutputStream.Flush(); } public override string GetHttpVerbName() { return _context.Request.HttpMethod; } public override string GetHttpVersion() { return string.Format("HTTP/{0}.{1}", _context.Request.ProtocolVersion.Major, _context.Request.ProtocolVersion.Minor); } public override string GetLocalAddress() { return _context.Request.LocalEndPoint.Address.ToString(); } public override int GetLocalPort() { return _context.Request.LocalEndPoint.Port; } public override string GetQueryString() { string queryString = ""; string rawUrl = _context.Request.RawUrl; int index = rawUrl.IndexOf('?'); if (index != -1) queryString = rawUrl.Substring(index + 1); return queryString; } public override string GetRawUrl() { return _context.Request.RawUrl; } public override string GetRemoteAddress() { return _context.Request.RemoteEndPoint.Address.ToString(); } public override int GetRemotePort() { return _context.Request.RemoteEndPoint.Port; } public override string GetUriPath() { return _context.Request.Url.LocalPath; } // remaining methods omitted // download sample for more details ... }

At this point, you may be tempted to go back to the previous sample and replace all instances of SimpleWorkerRequest with HttpListenerWorkerRequest. However, this would require you to pass the HttpListenerContext object into ProcessRequest. Unfortunately HttpListenerContext doesn't derive from MarshalByRefObject, which prevents it from crossing the AppDomain boundary. You'll need to redesign the sample to make this work.

First, you'll need a class that wraps an HttpListener object and makes it possible to control it from across the AppDomain. I've provided one in Figure 9 called HttpListenerWrapper. This is the type that you'll specify in the call to CreateApplicationHost from now on. It has a Configure method that instantiates the contained HttpListener object and registers the supplied URI prefixes. It has Start and Stop methods that simply delegate to the listener. And it has a ProcessRequest method that takes care of everything else—it calls GetContext, instantiates a new HttpListenerWorkerRequest, and passes it into HttpRuntime.ProcessRequest, which processes the request. You can use the following classes from the host app:

HttpListenerWrapper listener = (HttpListenerWrapper)ApplicationHost.CreateApplicationHost( typeof(HttpListenerWrapper), "/", Directory.GetCurrentDirectory()); listener.Configure(prefixes, "/", Directory.GetCurrentDirectory()); listener.Start(); while (true) listener.ProcessRequest();

Figure 9 HttpListenerWrapper Implementation

public class HttpListenerWrapper : MarshalByRefObject { private HttpListener _listener; private string _virtualDir; private string _physicalDir; public void Configure(string[] prefixes, string v, string p) { _virtualDir = v; _physicalDir = p; _listener = new HttpListener(); foreach (string prefix in prefixes) _listener.Prefixes.Add(prefix); } public void Start() { _listener.Start(); } public void Stop() { _listener.Stop(); } public void ProcessRequest() { HttpListenerContext ctx = _listener.GetContext(); HttpListenerWorkerRequest workerRequest = new HttpListenerWorkerRequest(ctx, _virtualDir, _physicalDir); HttpRuntime.ProcessRequest(workerRequest); } }

You now have a fully functional host for ASMX endpoints. There's another complete sample in the download that illustrates this code in action. While the console app is running, you should now be able to invoke the ASMX WebMethods using SOAP or through the form provided on the HTML documentation page.

Hosting Models: Web Services on the Desktop

Now that you know how to host ASMX in a process of your choice, you're obviously wondering when and where you should use this technique. There are a variety of situations in which it's unacceptable to run IIS on a particular machine, but you may still want to host Web services on that node using the ASMX programming model. When this is the case, the technique discussed in this column is a good alternative.

Unlike inetinfo.exe, which comes with numerous services for different types of communication, http.sys is a simplified kernel focused exclusively on HTTP traffic. Thus it has a reduced attack surface. Assuming you have either Windows XP SP2 or Windows Server 2003 on your machine, you can host ASMX in your own process without IIS. This means you are able to host ASMX in a console application, a Windows Forms application, or a Windows NT® service, depending on your particular needs.

However, I should point out that when you take this approach, you're giving up the sophisticated process model provided by IIS through the w3wp.exe (or aspnet_wp.exe) worker process. This means you lose things like process management (startup, failure detection, recycling), thread pool management, and ISAPI support. When you host ASMX, you're providing the process, so you're re-sponsible for providing the process model and associated services.

Figure 10 Web Services on the Desktop

Figure 10** Web Services on the Desktop **

Probably the most compelling use case for hosting ASMX in your own process is when you need Web services to run on the desktop. For example, you may have a Windows Forms application that needs to receive notifications from a Web server or some other internal enterprise-wide Windows service (see Figure 10). On the desktop, you don't need a sophisticated process model like you do on the server, but it would still be nice to take advantage of the productive programming model of ASMX. Http.sys and ASMX hosting is a great fit for this scenario. I've provided a few additional samples for download (a Windows Forms application and a Windows service that host ASMX) to illustrate these ideas.

Where Are We?

I've covered the basics for hosting the ASP.NET HTTP pipeline in a process of your choice. I've also discussed how to leverage http.sys and the new managed HttpListener class (and friends) to build a Web server right into your application. HttpListener makes it very easy to receive HTTP messages and forward them to the ASP.NET page for processing. Using these techniques gives you tremendous flexibility in running ASMX everywhere.

For more on this topic, see "New Features for Web Service Developers in Beta 1 of the .NET Framework 2.0", "HTTP.sys Response Cache", and "Download ASP.NET Cassini Sample Web Server".

Send your questions and comments for Aaron to

Aaron Skonnard is a co-founder of Pluralsight, an education and content creation company, where he focuses on XML and Web services technologies. He's a contributing editor to MSDN Magazine and the author of several books, including Essential XML Quick Reference (Addison-Wesley, 2001).