Click here to Skip to main content
15,885,309 members
Articles / Web Development / IIS
Article

Developing Non-ASP.NET Websites running under IIS

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
9 Jul 2017CPOL19 min read 15.3K   72   4   4
Discovering some of the nuances of IIS and looking under the hood at how Katana/Owin does its initialization.

Contents

Introduction

I don't use ASP.NET for my own websites, preferring instead more neutral web development tools, meaning, no Razor engine or other Microsoft-y things.  While I've implemented my own web server in C#, there's no reason that the "serving" and back-end route handling couldn't be implement in Python, node.js, or other framework, leaving the website content pretty much unchanged.  That said, I need to be able to run my web servers under IIS so that, as I wrote about in my last article, I can host multiple HTTPS domains.

And while I've never written any articles about the web server code, it does leverage the code base in The Clifton Method and is on GitHub.  My web server code does the basic things:

  • routing (semantic, meaning that the route handler is triggered by the data packet, not the route URL)
  • authentication
  • authorization
  • session management
  • content serving
  • error handling/reporting

in an implementation designed to leverage multiple processors/threading for each stage of the route handler workflows.  It's also very modular, for example, this is the configuration file for one of my websites that defines the modules used for logging, error handling, request routing, etc:

<Modules>
  <Module AssemblyName='Clifton.AppConfigService.dll'/>
  <Module AssemblyName='Clifton.ConsoleCriticalExceptionService.dll'/>
  <Module AssemblyName='Clifton.PaperTrailAppLoggerService.dll'/>
  <Module AssemblyName='Clifton.EmailService.dll'/>
  <Module AssemblyName='Clifton.SemanticProcessorService.dll'/>
  <Module AssemblyName='Clifton.WebRouterService.dll'/>
  <Module AssemblyName='Clifton.WebResponseService.dll'/>
  <Module AssemblyName='Clifton.WebFileResponseService.dll'/>
  <Module AssemblyName='Clifton.WebWorkflowService.dll'/>
  <Module AssemblyName='Clifton.WebDefaultWorkflowService.dll'/>
  <Module AssemblyName='Clifton.WebSessionService.dll'/>
  <Module AssemblyName='Clifton.WebServerService.dll'/>
</Modules>

All the pieces are implemented as services, with a publisher/subscriber pattern (that's the SemanticProcessorService) used to communicate between the services.

However, the purpose of this article is not to talk about my web server tech, but rather the trials, tribulations, and discoveries in switching over that last module to:

<Module AssemblyName='Clifton.IISService.dll'/>

so that the website runs as custom HTTP module in IIS.  The things I learned along the way are, well, "interesting."  To be honest about it, this article is mostly of interest to me for the sole purpose of documenting what I had to do to get my web server tech to work.  However, the things I learned about IIS and Katana/Owin are things that everyone should know if they attempt to do what I'm doing, or are just curious about the nuances of IHttpModule, IHttpHandler, IHttpAsyncHandler, and the initialization process of Katana/Owin.  So hopefully there's something here for you too!

Discovery

Let's start by creating a basic HTTPModule implementation, borrowed from the MSDN example here (Google search "Custom HttpModule Example" if the link changes.) 

Create the ASP.NET Web Application Project

You want to do this from the Add New Project dialog, selecting ASP.NET Web Application (.NET Framework):

Image 1

Then select an empty project:

Image 2

Create the HttpModule Handler

Add a C# file to the resulting project, I called mine "Hook":

using System;
using System.IO;
using System.Web;

public class Module : IHttpModule
{
  public void Init(HttpApplication application)
  {
    File.Delete(@"c:\temp\out.txt");
    application.BeginRequest += new EventHandler(BeginRequest);
    application.EndRequest += new EventHandler(EndRequest);
  }

  public void Dispose()
  {
  }

 private void BeginRequest(object sender, EventArgs e)
  { 
    HttpApplication application = (HttpApplication)sender;
    HttpContext context = application.Context;
    File.AppendAllText(@"c:\temp\out.txt", "BeginRequest: " + context.Request.Url + "\r\n");
    context.Response.ContentType = "text/html";
    context.Response.Write("<h1><font color=red>HelloWorldModule: Beginning of Request</font></h1><hr>");
  }

  private void EndRequest(object sender, EventArgs e)
  {
    HttpApplication application = (HttpApplication)sender;
    HttpContext context = application.Context;
    File.AppendAllText(@"c:\temp\out.txt", "EndRequest: " + context.Request.Url + "\r\n");
    context.Response.Write("<hr><h2><font color=blue>HelloWorldModule: End of Request</font></h2>");
  }
}

Note the File output (sending output to the debug output window is temperamental), and the assumption that you have a c:\temp folder.

Update The Web.config File

Modify the Web.config file, adding the module (you can remove all the other stuff):

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5.2"/>
    <httpRuntime targetFramework="4.5.2"/>
  </system.web>
  <system.webServer>
    <modules>
      <add name="Demo" type="Module, iisdemo"/>
    </modules>
  </system.webServer>
</configuration>

Create an index.html File

Add the file index.html with whatever content you like (something simple), like:

<p>Hello World!</p>

Review Where We're At

Your project should look like this (I added the index.html to the project):

Image 3

And your project folders should look similar to this:

Image 4

If you want, you can delete the "roslyn" folder and remove the reference to Microsoft.CodeDom.Providers.DotNetCompiler.CSharpCodeProvider, as we won't be needing them.

Exploring the Results

When you run the module, you should see:

Image 5

Inspect the Trace Output

Notice that this run in Edge.  Now look at the output file:

BeginRequest: http://localhost:63302/index.html
EndRequest: http://localhost:63302/index.html
EndRequest: http://localhost:63302/
EndRequest: http://localhost:63302/

That in itself should be eye opening, as we get several EndRequest calls that don't match BeginRequest calls. 

Now run the exact same URL in Chrome and look at the output file:

BeginRequest: http://localhost:63302/index.html
EndRequest: http://localhost:63302/index.html
EndRequest: http://localhost:63302/
EndRequest: http://localhost:63302/
BeginRequest: http://localhost:63302/favicon.ico
EndRequest: http://localhost:63302/favicon.ico

OK, the only difference there is that Chrome is also requesting favicon.ico.

What Happens When We Remove Index.html?

I put my web pages in a different location that IIS expects, so I deleted the index.html file.  To my surprise, we now see this:

Image 6

It looks like BeginRequest call is no longer being made!  Actually, it is -- look at the out.txt file:

BeginRequest: http://localhost:63302/
BeginRequest: http://localhost:63302/
EndRequest: http://localhost:63302/
EndRequest: http://localhost:63302/

There it is, but what we're output to the response stream is magically disappearing!  This is really bizarre behavior and demonstrates that I don't have a deep enough understanding of how the IIS pipeline works, particularly in regards to "error" conditions.

Set up IIS for the Web App

Now let's have some more fun.  Set up the same app in IIS:

Image 7

Inspect the Trace Output

What do we see now?

Edge:

BeginRequest: http://localhost/index.html
EndRequest: http://localhost/index.html
EndRequest: http://localhost/
EndRequest: http://localhost/

No change!

But in Chrome:

BeginRequest: http://localhost/
BeginRequest: http://localhost/
BeginRequest: http://localhost/index.html
EndRequest: http://localhost/index.html
EndRequest: http://localhost/
EndRequest: http://localhost/
BeginRequest: http://localhost/favicon.ico
EndRequest: http://localhost/favicon.ico

But sometimes we see this (and probably other variations):

BeginRequest: http://localhost/index.html
EndRequest: http://localhost/index.html
EndRequest: http://localhost/
EndRequest: http://localhost/
BeginRequest: http://localhost/favicon.ico
EndRequest: http://localhost/favicon.ico

What Happens When we Add an HttpHandler?

Now let's add a do nothing HttpHandler and wire it in to Web.config:

public class Handler : IHttpHandler
{
  public bool IsReusable { get { return true; } }

  public void ProcessRequest(HttpContext context)
  {
    File.AppendAllText(@"c:\temp\out.txt", "ProcessRequest: " + context.Request.Url + "\r\n");
    context.Response.Write("<p>Hello from the handler!</p>");
  }
}

This handler doesn't do anything with regards to writing to the response stream, but it does log when it gets called.

Web.config portion:

<system.webServer>
  <modules>
    <add name="Demo" type="Module, iisdemo"/>
  </modules>
  <handlers>
    <add name="Test" verb="*" path="*" type="Handler, iisdemo"/>
  </handlers>
</system.webServer>

Run the Web App Again (IIS Express)

Run the web app, and notice that whatever you put into index.html is no longer rendered:

Image 8

Inspect the Trace Output

Now look at the trace output file:

Edge:

BeginRequest: http://localhost:63302/
ProcessRequest: http://localhost:63302/
EndRequest: http://localhost:63302/

Chrome:

BeginRequest: http://localhost:63302/
ProcessRequest: http://localhost:63302/
EndRequest: http://localhost:63302/
BeginRequest: http://localhost:63302/favicon.ico
ProcessRequest: http://localhost:63302/favicon.ico
EndRequest: http://localhost:63302/favicon.ico

OK, wow, the begin/end requests now match!  So adding an HttpHandler changes the IIS pipeline to a more consistent begin/end request process!  How bizarre is that?  Also notice that the ProcessRequest occurs inbetween the begin/end requests!

Why This Matters?

It matters because I want to handle the HttpResponse content myself in a sane manner.  That means that I expect the IIS pipeline that generates the begin/end requests to behave sanely, and from what I've discovered here, sanity means including an HttpHandler, even if it doesn't nothing.

Do We Still Need Index.html?

No!  Once the IHttpHandler is implemented, we again see BeginRequest, even though there's no index.html file:

Image 9

In fact, the index.html file is now ignored, even if the handler doesn't write anything.  Commenting out the Response.Write:

public void ProcessRequest(HttpContext context)
{
  File.AppendAllText(@"c:\temp\out.txt", "ProcessRequest: " + context.Request.Url + "\r\n");
  // context.Response.Write("<p>Hello from the handler!</p>");
}

results in:

Image 10

The lesson learned here is, understand very carefully what IIS is doing based on the modules, handlers, and files.

How Do Others Do It - Katana/Owin?

This got me wondering, how are other implementations that implement their own web server hook into IIS?  I decided to take a look at the open source project AspNetKatana, Microsoft's OWIN implementation.  Here's what I found in the folder Microsoft.Owin.Host.SystemWeb.

First, there's an implementation of the IHttpModule:

internal sealed class OwinHttpModule : IHttpModule

As an aside, be very careful when reading this code:

public void Init(HttpApplication context)

The variable context is actually an HttpApplication, not an HttpContext!!!  Bad Microsoft!  This is very misleading if you don't read the code carefully!

This class initializes the IntegratedPipelineContext, which hooks into a whole bunch of the IIS pipeline processes:

public void Initialize(HttpApplication application)
{
  for (IntegratedPipelineBlueprintStage stage = _blueprint.FirstStage; stage != null; stage = stage.NextStage)
  {
    var segment = new IntegratedPipelineContextStage(this, stage);
    switch (stage.Name)
    {
      case Constants.StageAuthenticate:
      application.AddOnAuthenticateRequestAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostAuthenticate:
      application.AddOnPostAuthenticateRequestAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StageAuthorize:
      application.AddOnAuthorizeRequestAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostAuthorize:
      application.AddOnPostAuthorizeRequestAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StageResolveCache:
      application.AddOnResolveRequestCacheAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostResolveCache:
      application.AddOnPostResolveRequestCacheAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StageMapHandler:
      application.AddOnMapRequestHandlerAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostMapHandler:
      application.AddOnPostMapRequestHandlerAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StageAcquireState:
      application.AddOnAcquireRequestStateAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostAcquireState:
      application.AddOnPostAcquireRequestStateAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePreHandlerExecute:
      application.AddOnPreRequestHandlerExecuteAsync(segment.BeginEvent, segment.EndEvent);
      break;
    default:
      throw new NotSupportedException(
      string.Format(CultureInfo.InvariantCulture, Resources.Exception_UnsupportedPipelineStage, stage.Name));
    }
  }
  // application.PreSendRequestHeaders += PreSendRequestHeaders; // Null refs for async un-buffered requests with bodies.
  application.AddOnEndRequestAsync(BeginFinalWork, EndFinalWork);
}

Notice that this code does not hook BeginRequest!  In fact, searching the entire code base, nothing touches BeginRequest.

There's also an async handler for IHttpHandler:

public sealed class OwinHttpHandler : IHttpAsyncHandler

We note that the synchronous handler throws an exception, while the asynchronous handler calls into BeginProcessRequest:

void IHttpHandler.ProcessRequest(HttpContext context)
{
  // the synchronous version of this handler must never be called
  throw new NotImplementedException();
}

IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
  return BeginProcessRequest(new HttpContextWrapper(context), cb, extraData);
}

Owin creates it's own "context" which it then "executes":

OwinCallContext callContext = appContext.CreateCallContext(
  requestContext,
  requestPathBase,
  requestPath,
  callback,
  extraData);

try
{
  callContext.Execute();
}
catch (Exception ex)
{
  if (!callContext.TryRelayExceptionToIntegratedPipeline(true, ex))
  {
    callContext.Complete(true, ErrorState.Capture(ex));
  }
}

The callContext.Execute calls AppFunc, passing in a bunch of context information and returning a Task, which is defined as:

using AppFunc = Func<IDictionary<string, object>, Task>;

And AppFunc is also a property that is set with a builder (this is an odd call, why not use a generics like builder.Build<AppFunc>() ???):

AppFunc = (AppFunc)builder.Build(typeof(AppFunc));

This eventually calls into BuildInternal that invokes a delegate defined in a tuple.  The invocation is:

app = middlewareDelegate.DynamicInvoke(invokeParameters);

obtained from this tuple:

private readonly IList<Tuple<Type, Delegate, object[]>> _middleware;

which is processed in reverse order, and oddly, it will return a single app even though it potentially creates any app instances:

foreach (var middleware in _middleware.Reverse())
{
  Type neededSignature = middleware.Item1;
  Delegate middlewareDelegate = middleware.Item2;
  object[] middlewareArgs = middleware.Item3;

  app = Convert(neededSignature, app);
  object[] invokeParameters = new[] { app }.Concat(middlewareArgs).ToArray();
  app = middlewareDelegate.DynamicInvoke(invokeParameters);
  app = Convert(neededSignature, app);
}

return Convert(signature, app);

What's with that?  Well, it's sneaky.  Notice that the previous app is passed in as the first parameter to next app:

object[] invokeParameters = new[] { app }.Concat(middlewareArgs).ToArray();

thus it's building a pipeline, which is why the list is processed in reverse order - the final item processed by the for loop is the first item in the pipeline.

All this middleware is created here:

 public IAppBuilder Use(object middleware, params object[] args)
{
  _middleware.Add(ToMiddlewareFactory(middleware, args));
  return this;
}

That method is the end of the line -- it's where you hook into the Owin engine to define your own pipeline and parameters to each node in the pipeline.  Pretty neat.  The code comments are really helpful too!

This article (of many on the topic of building an Owin pipeline, and if you prefer a CP article, here's an excellent one) gives you a couple examples.

This all reminds me of the default workflow I implemented in my web server engine using my WebWorkflowService:

ServiceManager.Get<IWebWorkflowService>().RegisterPreRouterWorkflow(new WorkflowItem<PreRouteWorkflowData>(PreRouter));
ServiceManager.Get<IWebWorkflowService>().RegisterPostRouterWorkflow(new WorkflowItem<PostRouteWorkflowData>(PostRouterInjectLayout));
ServiceManager.Get<IWebWorkflowService>().RegisterPostRouterWorkflow(new WorkflowItem<PostRouteWorkflowData>(PostRouterRendering));

There are other similarities as well -- my workflow executes workflow items asynchronously but uses a custom thread pool instead of Task.

What Did We Learn?

  1. We must implement and HttpHandler to get sane begin/process/end results from IIS
  2. Katana/Owin does all its pipeline processing in the HttpHandler.
  3. Investigate and understand how IIS behaves!
  4. Look at other code for examples and better understanding!

So that defines where/how I have to hook my web server into the IIS pipeline.

Bootstrapping

We have a couple options now that the begin/end request is consistent.  We can hook into EndRequest to provide responses through my web server, or we can add a mechanism that hooks in through the HttpHandler's ProcessRequest call.  The latter makes more sense, but there's a problem: the HttpModule's Init call is made before the HttpHandler object is instantiated.  A little refactoring of the demo code:

public class Handler : IHttpHandler
{
  public bool IsReusable { get { return true; } }

  public Handler()
  {
    File.AppendAllText(@"c:\temp\out.txt", "Handler Instantiated.\r\n");
  }

  public void ProcessRequest(HttpContext context)
  {
    File.AppendAllText(@"c:\temp\out.txt", "ProcessRequest: " + context.Request.Url + "\r\n");
  }
}

public class Module : IHttpModule
{
  public void Init(HttpApplication application)
  {
    File.AppendAllText(@"c:\temp\out.txt", "HttpModule.Init called.\r\n");
    application.BeginRequest += new EventHandler(BeginRequest);
    application.EndRequest += new EventHandler(EndRequest);
  }
...

And we see:

HttpModule.Init called.
HttpModule.Init called.
BeginRequest: http://localhost:63302/
Handler Instantiated.
ProcessRequest: http://localhost:63302/
EndRequest: http://localhost:63302/

The order of the module and handler definitions in the Web.config file doesn't matter.  We do note, that since the handler is reusable, a subsequent call does not instantiate the handler again:

HttpModule.Init called.
HttpModule.Init called.
BeginRequest: http://localhost:63302/
Handler Instantiated.
ProcessRequest: http://localhost:63302/
EndRequest: <a href="http://localhost:63302/">http://localhost:63302/</a>
--- page refresh ---
BeginRequest: http://localhost:63302/
ProcessRequest: http://localhost:63302/
EndRequest: http://localhost:63302/

If indicate that the handler is not re-usable, we see:

HttpModule.Init called.
HttpModule.Init called.
BeginRequest: http://localhost:63302/
Handler Instantiated.
ProcessRequest: http://localhost:63302/
EndRequest: <a href="http://localhost:63302/">http://localhost:63302/</a>
--- page refresh ---
BeginRequest: http://localhost:63302/
Handler Instantiated.
ProcessRequest: http://localhost:63302/
EndRequest: http://localhost:63302/

Here the handler gets instantiated for each page refresh / navigation.

What Does Katana Do?

So how does Katana initialize something like _appAccessor which is used in the HttpHandler's BeginProcessRequest method:

OwinAppContext appContext = _appAccessor.Invoke();

Well, it does it by providing a separate internal constructor to the public constructor that IIS calls:

public OwinHttpHandler()
  : this(Utils.NormalizePath(HttpRuntime.AppDomainAppVirtualPath), OwinApplication.Accessor)
{
}

... 

internal OwinHttpHandler(string pathBase, Func<OwinAppContext> appAccessor)
{
  _pathBase = pathBase;
  _appAccessor = appAccessor;
}

And OwinApplication.Acessor returns a Lazy instantiation of the OwinBuilder.Build instance (notice the static):

private static Lazy<OwinAppContext> _instance = new Lazy<OwinAppContext>(OwinBuilder.Build);

...

internal static Func<OwinAppContext> Accessor
{
  get { return () => _instance.Value; }
  ...
}

...which is a static method:

 internal static OwinAppContext Build()
{
  Action<IAppBuilder> startup = GetAppStartup();
  return Build(startup);
}

...which, even though uncommented, appears to initialize the assembly loader, load the assemblies specified in owin:AppStartup in the app settings, and returns the startup Action.

Whew! 

What Would I Do?

The use of the internal constructor is a nice technique which I can leverage for my startup process, like this:

public class Handler : IHttpHandler
{
  public Bootstrapper bootstrapper;
  public ServiceManager serviceManager;

  public bool IsReusable { get { return true; } }

  public Handler() : 
    this(MyWebServerInitialization)
  {
  }

  internal Handler(Action<Handler> initializer)
  {
    initializer(this);
    FinishInitialization();
  }

  public void ProcessRequest(HttpContext context)
  {
    IWebServerService server = serviceManager.Get<IWebServerService>();
    server.ProcessRequest(context);
  }

  private static void MyWebServerInitialization(Handler handler)
  {
    handler.bootstrapper = new Bootstrapper();
    handler.serviceManager = handler.bootstrapper.Bootstrap(@"c:\websites\projourn\data", @"c:\websites\projourn\bin");
    ISemanticProcessor semProc = handler.serviceManager.Get<ISemanticProcessor>();

    // Required unless we wait for the SemanticProcessor's tasks to complete.
    semProc.ForceSingleThreaded = true; 
  }

  private void FinishInitialization()
  {
    InitializeDatabaseContext();
    InitializeRoutes();
    RegisterRouteReceptors();
  }
  ...

(Ignore the hardcoded literal strings, this is for a site for testing IIS.)

We'll look at why I force the semantic processor (the pubsub system) into single threaded mode later.

The interesting thing about this is, we don't need to initialize a module in Web.config, only the handler:

<handlers>
  <add name="All" verb="*" path="*" type="Handler, ProjournHttpModule"/>
</handlers>

(In the above code, that's the assembly name for the initialization for my ProJourn website.)

Image 11

Snazzy!  That works, though there's some things we're going to need to revisit.

HttpContext vs. HttpListenerContext

My next problem is that my web server uses an HttpListener, which returns an HttpListenerContext:

[fragment]
  listener = new HttpListener();
  listener.Start();
  Task.Run(() => WaitForConnection(listener));
[/fragment]

protected virtual void WaitForConnection(object objListener)
{
  HttpListener listener = (HttpListener)objListener;

  while (true)
  {
    // Wait for a connection. Return to caller while we wait.
    HttpListenerContext context = listener.GetContext();
    ...

IIS uses an HttpContext (and it's various ancillary classes, rather than HttpListener... classes) and Microsoft did not provide any common interface between the two.  This meant that I had to implement my own common interface for the HttpContext/HttpListenerContext, HttpRequest/HttpListenerRequest, and HttpResponse/HttpListenerResponse classes, as the former are in the System.Web namespace, and the latter are in the System.Net namespace.  Sigh.

public interface IContext
{
  IPAddress EndpointAddress();
  HttpVerb Verb();
  UriPath Path();
  UriExtension Extension();
  IRequest Request { get; }
  IResponse Response { get; }
  HttpSessionState Session { get; } 

  void Redirect(string url);
}

public interface IRequest
{
  NameValueCollection QueryString { get; }
}

public interface IResponse
{
  int StatusCode { get; set; }
  string ContentType { get; set; }
  Encoding ContentEncoding { get; set; }
  long ContentLength64 { get; set; }
  Stream OutputStream { get; }

  void Close();
  void Write(string data, string contentType = "text/text", int statusCode = 200);
  void Write(byte[] data, string contentType = "text/text", int statusCode = 200);
}

After an hour or so of refactoring and putting in the common methods/properties I need between the two implementations, I had that taken care.

Session State

The session object is important, as my web server manages session lifetime and authentication/authorization information for the session, in addition to whatever else the web app itself might require.  Because I only initialize the web server once, this was not a problem.  Working with IIS presents two problems:

  1. The IIS Session property of the context is null in the ProcessRequest call.  Why?
  2. IIS may fire up multiple copies of the web app, resulting in multiple, and separate instantiations of my web server, resulting in separate and distinct instances of my session manager.

Proof of #1:

Image 12

The solution to that problem was simple -- add the empty interface IRequiresSessionState to the handler class:

Image 13

There are two questions though:

  1. How to get a common session state between instances, or force a single instance, of my web server.
  2. Do I even want to use IIS's session object?

If the answer to #2 is "Yes", then the problem posed by #1 goes away because IIS manages the session object for the session and provides the correct session object in the context, regardless of which handler instance is running.  So that's the simplest way to go, and required very little modification of my session manager service:

protected SessionInfo CreateSessionInfoIfMissing(IContext context)
{
  SessionInfo sessionInfo;

  if (context is HttpListenerContextWrapper)
  {
    IPAddress addr = context.EndpointAddress();

    if (!sessionInfoMap.TryGetValue(addr, out sessionInfo))
    {
      sessionInfo = new SessionInfo(states);
      sessionInfoMap[addr] = sessionInfo;
    }
  }
  else
  {
    if (!context.Session.TryGetValue(SESSION_INFO, out sessionInfo))
    {
      sessionInfo = new SessionInfo(states);
      context.Session[SESSION_INFO] = sessionInfo;
    }
  }

  return sessionInfo;
}

Yes, this could be handled by a derived object, but implemented this way, I can use the same WebSessionService module in my modules list.  Notice that the IIS's session object is used only to store the key-value pairs of other objects in the session, for the simple reason that I implement some specific getters in my session manager:

public virtual T GetSessionObject<T>(IContext context, string objectName)
public virtual string GetSessionObject(IContext context, string objectName)
public virtual dynamic GetSessionObjectAsDynamic(IContext context, string objectName)

Suddenly, a Performance Problem!

There is one very disconcerting factor though with regards to using IIS's session state -- when actually accessing the session object, the performance is, well, terrible.  I suddenly went from a snappy website to one that was noticeably laggy (by at least a second or two) with accessing the session with HttpSessionState

So, let's review our two questions:

There are two questions though:

  1. How to get a common session state between instances, or force a single instance, of my web server.
  2. Do I even want to use IIS's session object?

The answer to #2 is "definitely not!"  This leaves solving question #1, and the easiest way to do this is with a couple static global variables:

public class Handler : IHttpHandler// , IRequiresSessionState <--- good riddance!
{
  public static Bootstrapper bootstrapper;
  public static ServiceManager serviceManager;

  public bool IsReusable { get { return true; } }

  public Handler() : 
    this(MyWebServerInitialization)
  {
  }

  internal Handler(Action<Handler> initializer)
  {
    initializer(this);
  }

  public void ProcessRequest(HttpContext context)
  {
    IWebServerService server = serviceManager.Get<IWebServerService>();
    server.ProcessRequest(context);
  }

  private static void MyWebServerInitialization(Handler handler)
  {
    if (bootstrapper == null)
    {
      bootstrapper = new Bootstrapper();
      serviceManager = bootstrapper.Bootstrap(@"c:\websites\projourn\data", @"c:\websites\projourn\bin");
      ISemanticProcessor semProc = serviceManager.Get<ISemanticProcessor>();

      // Required unless we wait for the SemanticProcessor's tasks to complete.
      semProc.ForceSingleThreaded = true;
      handler.FinishInitialization();
    }
  }
...

Yay!  Now my session state is working and my snappy performance is back!  Though, just for the record, it's still not as snappy as my non-IIS web server, but that might be the fault of IIS-Express.  I notice that using IIS with the actual domain name seems to be faster than using IIS-Express locally!  Or maybe that's because I'm using Edge with IIS-Express and Chrome to browse to the actual website.  Regardless, the difference is not significant enough to worry about.

What Did We Learn?

  1. Don't buy into the "Microsoft way" particularly with regards to IIS without carefully looking what you're buying into.
  2. Implement only what you need -- going down the HttpModule route was a rabbit hole, the right way to do this is with an HttpHandler implementation.
  3. Application initialization is an interesting problem when something else is doing the class construction and you don't have your application initialized yet - I've never had to use the class : this() syntax before in all my years of C# coding.

Threading and Multiple Handlers

Remember this?

// Required unless we wait for the SemanticProcessor's tasks to complete.
semProc.ForceSingleThreaded = true;

This exists because in my web server's workflow, the context is passed to each item in the workflow.  Any workflow item can close the response stream and terminate the workflow, and exceptions occurring at any step result in an exception handler gracefully dealing with the context response stream.  Furthermore, because the request handler (again, in my server) never cares about how or when the request terminates, it doesn't do anything else but post the request to the publisher.  The pubsub service, unless told otherwise, will fire off the process handlers of any interested receivers asynchronously using my own thread pool rather than using Task which is horribly tied to the number of processors and the .NET thread pool architecture which is terrible. 

Unfortunately, this by-intent design on my part cannot be used because when ProcessRequest is called by IIS, something from my server needs to have been put into the response stream before ProcessRequest returns back to IIS.  If not, there is a very good likelihood that IIS's thread will close the stream before some thread on my server has finished with the request.  Fortunately, since my pubsub architecture handles both asynchronous and "force as synchronous" requests, it was easy to modify for use with IIS, but I'm not happy with the solution.

One thing does bother me though -- those two statics that filter everything down to one instance of my web server:

public static Bootstrapper bootstrapper;
public static ServiceManager serviceManager;

The only reason for this is to deal with the need to actually share the session manager between all possible instances.  So, a little refactoring of the initialization process:

private static void MyWebServerInitialization(Handler handler)
{
  if (bootstrapper == null)
  {
    bootstrapper = new Bootstrapper();
    serviceManager = bootstrapper.Bootstrap(@"c:\websites\projourn\data", @"c:\websites\projourn\bin");
    ISemanticProcessor semProc = serviceManager.Get<ISemanticProcessor>();

    // Required unless we wait for the SemanticProcessor's tasks to complete.
    semProc.ForceSingleThreaded = true;
    handler.FinishInitialization(serviceManager);
  }
  else
  {
    Bootstrapper newBootstrapper = new Bootstrapper();
    ServiceManager newServiceManager = newBootstrapper.Bootstrap(@"c:\websites\projourn\data", @"c:\websites\projourn\bin");
    var sessionService = serviceManager.Get<IWebSessionService>();
    newServiceManager.RegisterSingleton<IWebSessionService>(sessionService);
    ISemanticProcessor semProc = newServiceManager.Get<ISemanticProcessor>();

    // Required unless we wait for the SemanticProcessor's tasks to complete.
    semProc.ForceSingleThreaded = true;
    handler.FinishInitialization(newServiceManager);
  }
}

Notice this part:

var sessionService = serviceManager.Get<IWebSessionService>();
newServiceManager.RegisterSingleton<IWebSessionService>(sessionService); 

Here, we get the session service from the first initialization and set it in the new server instance, replacing the session service instance that was created in the second (and subsequent) server instance.  I like this implementation much better!

Big Assumption Here

I am making the assumption here (and in the previous implementation) that the Handler constructor is called synchronously!  I can find no guidance on whether my assumption here is correct, but if we want to be really safe, we can use a lock:

private static Bootstrapper bootstrapper;
private static ServiceManager serviceManager;
private static object locker = new Object();

public Handler() : 
  this(MyWebServerInitialization)
{
}

internal Handler(Action<Handler> initializer)
{
  lock (locker)
  {
    initializer(this);
  }
}

Someone at Microsoft will probably scream when they see that.

What About IHttpAsyncHander?

Samuel Neff on Stack Overflow has a great explanation, which I quote in its entirety here:

ASP.NET uses the thread pool to process incoming HTTP requests.

When an IHttpHandler is called, a thread pool thread is used to run that request and the same thread is used to process the entire request. If that request calls out to a database or another web service or anything else that can take time, the thread pool thread waits. This means thread pool threads spend time waiting on things when they could be used to process other requests.

In contrast, when an IHttpAsyncHandler, a mechanism exists to allow the request to register a callback and return the thread pool thread to the pool before the request is fully processed. The thread pool thread starts doing some processing for the request. Probably calls some async method on a database call or web service or something and then registers a callback for ASP.NET to call when that call returns. At that point, the thread pool thread that was processing the HTTP request is returned to the pool to process another HTTP request. When the database call or whatever does come back, ASP.NET triggers the registered callback on a new thread pool thread. The end result is you don't have thread pool threads waiting on I/O bound operations and you can use your thread pool more efficiently.

For very high concurrency applications (hundreds or thousands of truly simultaneous users), IHttpAsyncHandler can provide a huge boost in concurrency. With a smaller number of users, there can still be a benefit if you have very long running requests (like a Long Polling request). However, programming under the IHttpAsyncHandler is more complicated, so shouldn't be used when it isn't really needed.

With a little refactoring and tweaked copy&paste from MSDN, I can take advantage of IHttpAsyncHandler as well:

public class Handler : /*IHttpHandler,*/ IHttpAsyncHandler
{
  ...
  public void ProcessRequest(HttpContext context)
  {
    throw new InvalidOperationException();
    // Gone:
    //IWebServerService server = serviceManager.Get<IWebServerService>();
    //server.ProcessRequest(context);
  }

  public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)
  {
    // The new way!
    return new AsyncServer(cb, context, extraData).ServeRequest(serviceManager);
  }
  ...

Now the work is done in my new AsyncServer class:

public class AsyncServer : IAsyncResult
{
  private AsyncCallback callback;
  private HttpContext context;

  public bool IsCompleted { get; protected set; }
  public WaitHandle AsyncWaitHandle => null;
  public object AsyncState { get; protected set; }
  public bool CompletedSynchronously => false;

  public AsyncServer(AsyncCallback callback, HttpContext context, Object state)
  {
    this.callback = callback;
    this.context = context;
    AsyncState = state;
    IsCompleted = false;
  }

  public AsyncServer ServeRequest(ServiceManager serviceManager)
  {
    ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask), serviceManager);

    return this;
  }

  private void StartAsyncTask(object sm)
  {
    ServiceManager serviceManager = (ServiceManager)sm;
    IWebServerService server = serviceManager.Get<IWebServerService>();
    server.ProcessRequest(context);
    IsCompleted = true;
    callback(this);
  }
}

Why no try-catch in StartAsyncTask?  Because I trust that my server workflow will handle the exceptions.

Again, I'm not particularly a fan of ThreadPool but for simplicity's sake (ok, I admit it, I have not sufficiently abstracted out my thread pool class!) I'm going with the stock example until I refactor my code so that my thread pool isn't embedded in the semantic processor service.

Conclusion

The great thing I find about writing an article like this are two things time and time again:

  1. I should look at how others are doing this and learn from them
  2. As a result of #1, I can do better myself.

This was so true with this article.  When I started writing this article, I had my web server working with IIS, I had dealt with the session management issue, and I had a really ugly wart in my code, something like:

if (context.Response.ContentType != null) return;

Why?  Because I was injecting my response in the EndRequest call and I hadn't fully understood what was going on with IIS.  As the very first example showed, I was getting multiple requests to the same URL.  My response service was throwing exceptions because once the content type was set, setting it again throws a .NET exception.  This wasn't just code smell, it was code reek.  I couldn't imagine that others had to do the same silliness, so I started looking around and trying different things with IIS, and soon I discovered the right way of doing this -- ah, the air was fresh again!

Another problem I kept dealing with was IIS's session manager.  When BeginRequest fires, the context's session doesn't exist.  By the time EndRequest fires, the context's session is gone -- see the chart here, which shows that the session state is acquired after BeginRequest and released before EndRequest in the HttpApplicationAcquireRequestState and HttpApplicationReleaseRequestState, respectively.  I really wanted to use IIS's session manager, thinking it would be better than what I had implemented, and to at least fold in some of the features my session manager provides.  That of course led to the discovery of the performance penalty of using IIS's session manager (I suspect this has to do with the serialization/persistence) which led me to not using IIS's session manager, though I didn't bother to investigate the performance issue much further -- I had a solution that was already working well, it just required a little tweaking to use with IIS.

I'd also gone down an interesting path of discovery -- that while I had no problems with routing POST verbs to their appropriate handlers with IIS-Express, my login from the get-go failed with IIS because my login is a POST jQuery call.  Why didn't work?  It turns out that by default IIS only honors GET verbs.  This led down the rabbit hole of adding a custom POST handler, which became completely unnecessary when I added my custom handler (note that this handler accepts all verbs for all paths):

<add name="Test" verb="*" path="*" type="Handler, iisdemo"/>

When writing this article, in the back of my mind was the nagging thought that what I write here is going be under scrutiny, the code smell was a red flag that I hadn't understood the problem well enough, and so as a result, I did some further digging, research, and rework to come up with what I believe is a correct solution and one that will pass muster.  Ah, the benefits of documentation!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionWhere to find HttpContextWrapper ? Pin
Ehsan Sajjad10-Jul-17 23:13
professionalEhsan Sajjad10-Jul-17 23:13 
AnswerRe: Where to find HttpContextWrapper ? Pin
Marc Clifton14-Jul-17 2:41
mvaMarc Clifton14-Jul-17 2:41 
GeneralMy vote of 5 Pin
Ehsan Sajjad9-Jul-17 5:41
professionalEhsan Sajjad9-Jul-17 5:41 
GeneralRe: My vote of 5 Pin
Marc Clifton9-Jul-17 11:27
mvaMarc Clifton9-Jul-17 11:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.