I have to admit it, I was really surprised this week. While investigating a mysterious issue I discovered that I knew less about the hosting platform of ASP.NET and IIS than I thought I did. What I found makes sense, but it was surprising nonetheless.
What I found has made me believe more strongly what I have recently been advocating. Affinity is dangerous. The model of pure functions in functional languages is much easier to understand and thus reason about. Whenever affinity is used as a back-door to rely on some previously established state, you are essentially adding input to your function, and when you do so you had better understand the immutability or otherwise of that information. The problem? Something believed to be immutable was not in fact immutable and thus the correctness of the code was gone.
Now this is all quite mysterious, so I’d probably get to telling you about what it is that I found.
I had a HttpModule that was impersonating a user, and therefore changing the return value of WindowsIdentity.GetCurrent(). I also caused a change in Thread.CurrentPrincipal because I wanted any .NET code in the ASP.NET pipeline to consider this account to be the current account. I thought everthing was fine! (I should point out that I am dubious about the quality and purpose of this code, it is just what I had when I was investigating. I suspect a rewrite is due…)
In fact, ASP.NET interleaves request tasks (note: request tasks, not just whole requests) on the same thread and therefore has logic to switch the current thread identity and impersonation behaviour. It only needs this because it interleaves request processing, otherwise it could have just left the identity as it was. The problem is the following: it does not determine the behaviour based on Thread.CurrentPrincipal or WindowsIdentity.GetCurrent(). Instead, the request’s execution context is represented by the HttpContext class and HttpContext.Current instance. The User property of HttpContext is actually an instance of IPrincipal and ASP.NET will undo impersonation before switching to a new task. Without setting the HttpContext.Current.User property, this impersonation approach is not going to work!
The solution is clearly trivial: set the HttpContext.Current.User property. However, that misses the point. Server-side code oftens requires the splitting up of work into smaller units. When this happens, each of these units of work may be executed on the same thread without interruption, on the same thread with infrastructure interruption and then an immediate resumption, on the same thread with the interleaving of an alternate unit of work or on another thread. Modern systems have a large amount of co-operative multi-tasking on the same thread. This is true for ASP.NET, WCF and the TPL. It also means that, when traversing threads intentionally, you have to take responsibility for taking this state with you.
The large and complex subsystems of .NET include several examples of this. The ExecutionContext manages the CLR state as it reuses different operating system threads. WCF has the OperationContext and ServiceSecurityContext classes. ASP.NET has the HttpContext. Of course, you’ve probably also used the SynchronisationContext to interact with a UI thread that has its own thread affinity.
In retrospect, a lot of this looks obvious. I knew that ASP.NET supported asynchronous page execution, and of course it may need to load the page from disk and even compile it in some cases, so an asynchronous approach seems obvious. Similarly, I’ve coded custom WCF bindings and so I know that they are also an asynchronous design. Nevertheless, it is all to easy to make the incorrect assumption that these methods and events are just executed as a monolithic block of code with the infrastructure providing the simplest of glue. The reality is far more complex.
Thanks go to Scott Hanselman for a nice blog post on some of this: System.Threading.Thread.CurrentPrincipal vs. System.Web.HttpContext.Current.User or why FormsAuthentication can be subtle. The Microsoft Patterns & Practices team also have a detailed description of ASP.NET authentication, although the article is quite old now: Explained: Windows Authentication in ASP.NET 2.0.