Sometimes you’re stuck taking old tech and merging it with new tech. The other day at work, I found myself wanting to self host an old ASMX web service from an simple WinForms app to support a simple simulator. I was able to find a great post from the co-founders of Pluralsight Aaron Skonnard, all be it an old post, fascinating none the less. I would recommend reading Aaron Skonnard’s article for more detail, in this post I will paraphrase the example with a link to my example code.
Download SelfHostedSoapService.zip
The reason why self hosting works so well now goes way back to Windows 2003/XP SP2. Microsoft had rewritten its protocol stack for HTTP handling, http.sys which is now at the kernel level. It is important to note that this gave rise to the abstracting .NET applications from being tied to IIS. This is what all of the cool ASP.NET MVC OWIN stuff is written on top of and allows us to write a “simple” self hosted web server. .NET 2.0 came out with managed classes to make this possible
- HttpListener – configures and starts an listener for HTTP requests
- HttpWorkerRequest - abstract class used by ASP.NET to process requests
- SimpleWorkerRequest – provides a simple implementation of HttpWorkerRequest to handle basic GET requests
- ApplicationHost.CreateApplicationHost – creates application domain for hosting ASP.NET (caveat is assemblies must be in the GAC or in the bin folder of relative to running process)
I created 3 projects to hose a simple Calculator.asmx web service example:
- Library – contains the HTTP listener and worker class
- Console – starts the web server on a given port
- WinFrom – uses the ASMX web service to execute calculator methods
Library contained 2 key classes.
HttpListenerWrapper.cs
This will host the HttpListener class so that we can run our process on a separate thread. The code for this is below.
[sourcecode language='csharp' padlinenumbers='true'] /// <summary> /// A wrapper for the HttpListener so we can start and stop our listener. /// </summary> /// <seealso cref="System.MarshalByRefObject" /> 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; Console.WriteLine($"Configuring HTTP listener with virtual directory [{v}] and physical directory [{p}]"); // the HttpListener that will be used to extract request _listener = new HttpListener(); // listener configuration for how http.sys will map incoming HTTP requests // can use http://*:8081 or http://+:8081. for production use, wildcards should never be used foreach (string prefix in prefixes) _listener.Prefixes.Add(prefix); } public void Start() { _listener.Start(); } public void Stop() { _listener.Stop(); } public void ProcessRequest() { // receives the incoming request for processing HttpListenerContext ctx = _listener.GetContext(); Console.WriteLine($"Request: {ctx.Request.RawUrl}"); ; // create our worker that will act as our web server and allow ASP.NET to process its pipeline HttpListenerWorkerRequest workerRequest = new HttpListenerWorkerRequest(ctx, _virtualDir, _physicalDir); // process the request HttpRuntime.ProcessRequest(workerRequest); } } [/sourcecode]
HttpListenerWorkerRequest.cs
This represents our web server that contains the methods needed to allow ASP.NET to process its pipeline.
[sourcecode language='csharp' ] /// <summary> /// Represents our web server which sets up the processing for ASP.NET /// </summary> /// <seealso cref="System.Web.HttpWorkerRequest" /> 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; } public override void SendKnownResponseHeader(int index, string value) { _context.Response.Headers[ HttpWorkerRequest.GetKnownResponseHeaderName(index)] = value; } public override void SendResponseFromMemory(byte[] data, int length) { _context.Response.OutputStream.Write(data, 0, length); } public override void SendStatus(int statusCode, string statusDescription) { _context.Response.StatusCode = statusCode; _context.Response.StatusDescription = statusDescription; } public override void SendUnknownResponseHeader(string name, string value) { _context.Response.Headers[name] = value; } public override void SendResponseFromFile( IntPtr handle, long offset, long length) { } public override void SendResponseFromFile( string filename, long offset, long length) { } // additional overrides public override void CloseConnection() { //_context.Close(); } public override string GetAppPath() { return _virtualDir; } public override string GetAppPathTranslated() { return _physicalDir; } public override int ReadEntityBody(byte[] buffer, int size) { return _context.Request.InputStream.Read(buffer, 0, size); } public override string GetUnknownRequestHeader(string name) { return _context.Request.Headers[name]; } public override string[][] GetUnknownRequestHeaders() { string[][] unknownRequestHeaders; System.Collections.Specialized.NameValueCollection headers = _context.Request.Headers; int count = headers.Count; List<string[]> headerPairs = new List<string[]>(count); for (int i = 0; i < count; i++) { string headerName = headers.GetKey(i); if (GetKnownRequestHeaderIndex(headerName) == -1) { string headerValue = headers.Get(i); headerPairs.Add(new string[] { headerName, headerValue }); } } unknownRequestHeaders = headerPairs.ToArray(); return unknownRequestHeaders; } public override string GetKnownRequestHeader(int index) { switch (index) { case HeaderUserAgent: return _context.Request.UserAgent; default: return _context.Request.Headers[GetKnownRequestHeaderName(index)]; } } public override string GetServerVariable(string name) { // TODO: vet this list switch (name) { case "HTTPS": return _context.Request.IsSecureConnection ? "on" : "off"; case "HTTP_USER_AGENT": return _context.Request.Headers["UserAgent"]; default: return null; } } public override string GetFilePath() { // TODO: this is a hack string s = _context.Request.Url.LocalPath; if (s.IndexOf(".aspx") != -1) s = s.Substring(0, s.IndexOf(".aspx") + 5); else if (s.IndexOf(".asmx") != -1) s = s.Substring(0, s.IndexOf(".asmx") + 5); return s; } public override string GetFilePathTranslated() { string s = GetFilePath(); s = s.Substring(_virtualDir.Length); s = s.Replace('/', '\\'); return _physicalDir + s; } public override string GetPathInfo() { string s1 = GetFilePath(); string s2 = _context.Request.Url.LocalPath; if (s1.Length == s2.Length) return ""; else return s2.Substring(s1.Length); } } [/sourcecode]
The Console application puts it all together. It’s a big tricky due to the requirements of ApplicationHost.CreateApplicationHost, so there are a few post build folder and copy setup needed to allow the ASMX to run correctly. Namely
- You must copy our library into a bin folder under where the Console executable will run
- You must copy the Calculator.asmx so it will be in the same folder as Console application executable
- You must copy the Calculator.asmx.cs file into a App_Code folder under where the Console executable will run
Here is the Program.cs of the Console application which fires up the HTTP listener to support the ASMX web service.
[sourcecode language='csharp' ] class Program { static bool run = true; static string port = ConfigurationManager.AppSettings["port"]; /// <summary> /// Example 1: self hosted web serivce using System.Web.Hosting.ApplicationHost. Supports full web ASMX web serivce /// </summary> static void Main(string[] args) { // run our web server ThreadPool.QueueUserWorkItem(RunListener); // wait for user to tell us to stop Console.ReadLine(); run = false; } static void RunListener(object state) { var currentDir = Directory.GetCurrentDirectory(); HttpListenerWrapper listener = (HttpListenerWrapper)ApplicationHost.CreateApplicationHost(typeof(HttpListenerWrapper), "/", currentDir); listener.Configure(new[] { $"http://localhost:{port}/", $"http://127.0.0.1:{port}/" }, "/", currentDir); listener.Start(); Console.WriteLine($"Listening for requests on http://localhost:{port}/"); while (run) listener.ProcessRequest(); listener.Stop(); } } [/sourcecode]
Building the Console application will yield the following output:
Running the application will yield the following:
Opening a browser and navigating to http://localhost:8080/Calculator.asmx will respond with web services definition page
The WinForm application used the WSDL from the web service to generate a proxy and call the simple functions on the Calculator. There is not error handling in the app to keep it simple.
And there you have it. Self hosting a old school ASMX web service. If you want to host a WCF web service this is a bit easier as there is plumbing out of the box in the System.ServiceModel library for this purpose. See Microsoft post here and ServiceHost class.