Contents
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!
Let's start by creating a basic HTTPModule implementation, borrowed from the MSDN example here (Google search "Custom HttpModule Example" if the link changes.)
You want to do this from the Add New Project dialog, selecting ASP.NET Web Application (.NET Framework):
Then select an empty project:
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.
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>
Add the file index.html with whatever content you like (something simple), like:
<p>Hello World!</p>
Your project should look like this (I added the index.html to the project):
And your project folders should look similar to this:
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.
When you run the module, you should see:
Notice that this run in Edge. Now look at the output file:
BeginRequest: http:
EndRequest: http:
EndRequest: http:
EndRequest: http:
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:
EndRequest: http:
EndRequest: http:
EndRequest: http:
BeginRequest: http:
EndRequest: http:
OK, the only difference there is that Chrome is also requesting favicon.ico.
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:
It looks like BeginRequest call is no longer being made! Actually, it is -- look at the out.txt file:
BeginRequest: http:
BeginRequest: http:
EndRequest: http:
EndRequest: http:
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.
Now let's have some more fun. Set up the same app in IIS:
What do we see now?
Edge:
BeginRequest: http:
EndRequest: http:
EndRequest: http:
EndRequest: http:
No change!
But in Chrome:
BeginRequest: http:
BeginRequest: http:
BeginRequest: http:
EndRequest: http:
EndRequest: http:
EndRequest: http:
BeginRequest: http:
EndRequest: http:
But sometimes we see this (and probably other variations):
BeginRequest: http:
EndRequest: http:
EndRequest: http:
EndRequest: http:
BeginRequest: http:
EndRequest: http:
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, and notice that whatever you put into index.html is no longer rendered:
Now look at the trace output file:
Edge:
BeginRequest: http:
ProcessRequest: http:
EndRequest: http:
Chrome:
BeginRequest: http:
ProcessRequest: http:
EndRequest: http:
BeginRequest: http:
ProcessRequest: http:
EndRequest: http:
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!
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.
No! Once the IHttpHandler
is implemented, we again see BeginRequest
, even though there's no index.html file:
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");
}
results in:
The lesson learned here is, understand very carefully what IIS is doing based on the modules, handlers, and files.
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.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)
{
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
.
- We must implement and
HttpHandler
to get sane begin/process/end results from IIS - Katana/Owin does all its pipeline processing in the
HttpHandler
. - Investigate and understand how IIS behaves!
- 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.
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:
Handler Instantiated.
ProcessRequest: http:
EndRequest: http:
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:
Handler Instantiated.
ProcessRequest: http:
EndRequest: <a href="http://localhost:63302/">http:
--- page refresh ---
BeginRequest: http:
ProcessRequest: http:
EndRequest: http:
If indicate that the handler is not re-usable, we see:
HttpModule.Init called.
HttpModule.Init called.
BeginRequest: http:
Handler Instantiated.
ProcessRequest: http:
EndRequest: <a href="http://localhost:63302/">http:
--- page refresh ---
BeginRequest: http:
Handler Instantiated.
ProcessRequest: http:
EndRequest: http:
Here the handler gets instantiated for each page refresh / navigation.
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!
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>();
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.)
Snazzy! That works, though there's some things we're going to need to revisit.
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)
{
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.
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:
- The IIS Session property of the context is null in the ProcessRequest call. Why?
- 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:
The solution to that problem was simple -- add the empty interface IRequiresSessionState
to the handler class:
There are two questions though:
- How to get a common session state between instances, or force a single instance, of my web server.
- 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)
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:
- How to get a common session state between instances, or force a single instance, of my web server.
- 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
{
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>();
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.
- Don't buy into the "Microsoft way" particularly with regards to IIS without carefully looking what you're buying into.
- 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. - 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.
Remember this?
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>();
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>();
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!
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.
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 : IHttpAsyncHandler
{
...
public void ProcessRequest(HttpContext context)
{
throw new InvalidOperationException();
}
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)
{
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.
The great thing I find about writing an article like this are two things time and time again:
- I should look at how others are doing this and learn from them
- 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!