Self Hosting an Old ASMX Web Service

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

  1. You must copy our library into a bin folder under where the Console executable will run
  2. You must copy the Calculator.asmx so it will be in the same folder as Console application executable
  3. 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:

image

Running the application will yield the following:

CalculateWebServiceConsole

Opening a browser and navigating to http://localhost:8080/Calculator.asmx will respond with web services definition page

CalculateWebServicePage

image

CalculateWebServiceAddMethodResult


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.

CalculateWinFormApp

 

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.

Download SelfHostedSoapService.zip

Docker - A New Way To Think Virtual But Not Be Virtual

At times you get the pleasure of working with some cutting edge technology that gives you the "wow that's pretty slick" feeling.  Other times it can be a rocky unstable mess.  Docker is one such technology that has not been the later.  The concept is simple, run this self contained little unit on bare metal as if it were a virtual machine.  The analogy many liken this too is a shipping yard with lots of containers.  The containers are the self contained packages and since all containers have the same shape the can be stacked and treated the same even though each container's internals can vary greatly.  Hence, one of the reasons Docker calls these units containers.

I've had success setting this up on Windows and a Mac, both of which use a tool called boot2docker.  Since Docker only runs on Linux systems, both Windows and Mac need to use Virtual Box to spin up a Linux machine that can host Docker.  For Windows check out https://docs.docker.com/installation/windows/ and for Mac see https://docs.docker.com/installation/mac/.  I had a little trouble at first on Windows, which required deleting the VM on disk then re-initializing docker.  After they are up and running, it's a breeze getting a container started, just type "docker run container name".  It's that simple.  Of course, if you want your container to expose ports and do other things there's a little more to it, but if you want to test out Linux based products, this is a very slick way to do it.  To see a list of docker supported containers go to https://registry.hub.docker.com/Pluralsight also has some good modules on how to use docker.

Atlasian SourceTree : A Great Git Visual Tool

Git is source control system that has risen to new heights.  Even Team Foundation Server no exposes Git.  Many use the Git command line operations but I always prefer the visual tools for day to day work.  Atlasian, produces of Jira, Confluence, Stash, Bamboo, have a tool call SourceTree which is a excellent tool for managing Git.  You can download it for Windows or Mac here http://www.sourcetreeapp.com/.

Install Mongo On Windows As a Service

Mongo is a common NoSql database that is flexible and easy to use.  It’s ability to scale and shard across many nodes makes it a great option for load balancing and scaling an application domain that fits a NoSql type schema.

1.  Download the latest version of Mongo that fits your platform (i.e. x86, x64).  Run the installer and place the application files in C:\Tools\MongoDB…  Follow my environment variable setup post for certain development folders.

2. Create an environment variable MONGO_HOME to point to the root of the mongo application directory.  If you followed my post on environment variable setup this would be %TOOLS_HOME%\MongoDB…  Add %MONGO_HOME%\bin to the Path environment variable.

3.  Create a directory to store the mongo data.  The simple approach is to create the directory C:\data\db.  This is Mongo’s default but can be changed using it’s configuration file.

4.  Create a log directory for the Mongo logs.  The simplest is to create “log” directory under the root Mongo directory C:\Tools\Mongo….

5.  Create a mongodb.conf file in C:\Tools\MongoDB… root directory.  This put the following minimum information in the file.

systemLog:
   destination: file
   path: "/Tools/MongoDB 2.6 Standard/log/mongodb.log"
net:
   bindIp: 127.0.0.1
   port: 27017
storage:
   journal:
      enabled: true
   dbPath: "/data/db"

 

6.  Open a command window (ensure it’s in Administrator mode) enter the following command.  This will work if your Path variable is set correctly.

mongodb.exe --config "C:\Tools\MongoDB 2.6 Standard\mongodb.conf" --install