Sunday, August 17, 2008

I've had an issue on two computers (my primary development machine and a virtual pc dev box) which I was finally able to solve today after many hours of frustrated searching and experimenting.  Interestingly, this issue only affected my two machines, but not those of a co-worker.  What was more peculiar was that I have higher privileges in TFS.  The issue revolved around getting the Team Foundation Server Team Explorer 2008 to recognize our automated builds.

Within Visual Studio 2008's Team Explorer pane I am able to browse all work items, documents, reports, etc, but not builds.  In fact, the node shows up with a red 'X' icon and is mislabeled 'Build' rather than the correct 'Builds'.

I attempted to fix it by uninstalling / reinstalling the TFS 2008 Power Tools (July Release), repairing my VS2008 installation, disabling my firewall, disabling my antivirus.  I tried digging into various configuration files and renaming / deleting my cache folder to no avail.

Ultimately, to fix the issue I had to resort to resetting my Visual Studio settings to their factory settings.  To do this, I did the following:

  1. Renamed/Deleted by TFS Cache folder.  On my Vista machine it's found in (C:\Users\[USERNAME]\AppData\Local\Microsoft\Team Foundation\2.0).
  2. Reset Visual Studio 2008 settings by typing devenv /resetuserdata from the command line.
Sunday, August 17, 2008 10:34:53 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, August 14, 2008

I upgraded my Visual Studio 2008 installation the other day to SP1 and everything went beautifully.  One of the issues that Microsoft fixed centered around the naming of embedded binary resource files.

Traditionally, binary resource files have an extension of .resources.  However, when VS2008 was introduced, it came with a bug that forced you to tack on a second extension: fileName.resources.resources.

Apparently, this is now fixed, but I did have to go back and rename my files accordingly.

Thursday, August 14, 2008 3:27:22 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, August 05, 2008

I finally gave in (for better or for worse) and signed up on facebook.com this past weekend.  I've always had an aversion to such social networking sites for one reason or another.  In fact, facebook.com and myspace.com have long been blocked on my firewall at home until just recently.  I may go back to block myspace.com, however; pretty much everything I've seen on it has been annoying, sleasy, trashy, and not worth my time - basically stuff I didn't want on my computer in the first place.

Now I don't know all the specifics about facebook.com, but I've had a great time on it in the past few days reestablishing connections with friends from my youth in New Mexico.  I've made contact with some people that I've not been able to find until now, which is very exciting.

I realize that I'm a late bloomer of a sort with regards to social networking, but such is the way of things I suppose.  Among other things, I've simply not had time for it.  I still don't have time for it, but I may make some for it, we'll see how it goes.

If you on facebook.com and you know me, drop me a line!

Tuesday, August 05, 2008 12:18:27 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [3]  |  Trackback
 Sunday, July 13, 2008

I've recently been thinking about database authentication in an ASP.NET application.  While this concept is most definitely unoriginal - a great many web applications must perform data access of some sort - it is easy to get confused by the many variations of data access.

To summarize in brief, there are two primary methods for authenticating to a database: Windows Authentication (also called Integrated Security), and Sql Authentication.  All SQL Servers support Windows Authentication and it's the natural choice.  Enabling Sql Authentication, on the other hand, requires that the server be configured to support it; this can be accomplished either during installation or after the fact.  What's more, not all organizations will support Sql Authentication.

Despite the aforementioned, perhaps he simplest technique to authenticate a user in the database is to use Sql Authentication.  It requires that 1) a login be created in the SQL Server and mapped to the database and 2) the connection string contain the appropriate User ID and Password parameters.  It is the simplest also because the database can be located locally or remotely and the authentication will succeed.

As soon as you use Windows Authentication in a web application the level of difficulty raises, even if only slightly.  To properly implement Windows Authentication in a web application, the identity of the process must be determined.  For purposes of this article, as it is not directly pertinent and can be quite large in scope, I will refrain from exploring the topic of ASP.NET process identity.  Suffice it to say that on a Windows Server 2003/2008 the default identity for an IIS application pool is NT AUTHORITY\NETWORK SERVICE.  On other platforms this identity will vary.

ASP.NET Application - Anonymous or Integrated Windows Authentication, No Impersonation

If the requesting user is not being impersonated, IIS will access resources using the identity of the application pool (e.g., NT AUTHORITY\NETWORK SERVICE).

ASP.NET Application - Anonymous Authentication, Impersonation Enabled

If your web application is using anonymous access and the user is being impersonated a couple of things can happen.  First, if, in your web.config file, you specify <identity impersonate="true" />, IIS will impersonate the anonymous user specified for your web site (e.g., IUSR_MACHINENAME, IUSR, etc.).  A user may be specified in the web.config file as well via <identity impersonate="true" userName="DOMAIN\User" password="xxx" />.  In this case, the web server will impersonate the user on the server.  NOTE: This user needs, at a minimum, Write access to the \Temporary ASP.NET Files folder.

ASP.NET Application - Integrated Windows Authentication, Impersonation Enabled

This is functionally similar to the previous item except if you use the simple form of the identity element (i.e., <identity impersonate="true" />), the user account that is performing the request is impersonated.  This is quite helpful especially if you need to control access to a server-side resource by ACL.

When connecting to a SQL Server database that is local to the web server, that is, it is physically on the same machine, you can grant access to the identity under which the web application is running (ASPNET, NT AUTHORITY\NETWORK SERVICE, IUSR_MACHINENAME, or the impersonated user for instance).

When the database is physically remote, however, care must be taken to properly flow user credentials to remote server.  That is, in fact, something of a misnomer.  Credentials aren't actually flowing to the remote server, but rather an authentication token.  This token is generated on the computer where the user identity is authenticated.

A user token generated on the web server will be able to flow to the remote machine without any extra work.  This token is created in the following scenarios:

  1. Using Basic authentication - the user is actually logged-in on the server.
  2. Using no impersonation - the website is locally authenticated as ASPNET or NT AUTHORITY\NETWORK SERVICE.
  3. The impersonation identity is manually set in the web.config's <identity /> element.

NTLM will permit the token a 'single hop' to the remote server.  Provided the identity in question has access to the database, the connection will be successfully established, and the requested information returned.

If, on the other hand, the web server is impersonating the requesting user, the solution is not so cut-and-dry.  The user's token is created on the client computer and makes a 'single hop' to the web server.  When making a request to a remote server a 'double hop' must be performed.  NTLM will prohibit the token from being passed to the server and you will encounter an error resembling "Login failed for user 'NT AUTHORITY\ANONYMOUS LOGON'".

This error may be confusing and disheartening to a developer who has not seen it before.  This is often the result of having tested the website on a development machine (which is usually local) - "It works on my machine."  Well, it works because the token is created locally and only needs a 'single hop' to get to the database server.

There are a few solutions that may help to address the oft-times confusing and frustrating 'double hop' conundrum.

  1. Setup Constrained Delegation
    Less far-sweeping than full delegation, Kerberos constrained delegation allows tokens to flow against a limited set of services.  This option is only available on Windows Server 2003+.  To properly implement constrained delegation, you must set up a Service Principal Name (SPN), identifying the service and the machine trusted for delegating the tokens.  While simple in principle, I've found this solution to be tempermental and very sensitive.  You may seemingly have everything set up correctly and still not get it to work.
  2. Fall back (a.k.a., revert) to the base IIS process's identity
    Having fought the 'double hop' issue more times than I care to admit, and for more hours than are healthy, this idea came to me last week while discussing the 'problem' with a friend and associate at Microsoft.  While this idea is not revolutionary nor original, it is definitely useful.  This technique allows your website to retain its ability to identify the calling user while deferring to the process's identity (e.g., NT AUTHORITY\NETWORK SERVICE) to access remote databases.

To make this (#2) work we need the ability to 'undo' the impersonation that ASP.NET performs and then reimpersonate when we're finished.  Unfortunately, this functionality is not native to the .NET Framework as far as I'm aware - it is, however, accessible via the RevertToSelf() Window API function.  In order to encapsulate the logic of reverting to the base process's identity and restoring impersonation, I've created a simple disposable class which is presented below:

/// <summary>
/// Represents a disposable class that, for the lifetime of the object, runs using the
/// underlying identity of the process.  This class is useful within an ASP.NET application
/// that is impersonating the caller, but needs to access network or directory resources
/// that would otherwise be prohibited without setting up constrained delegation in Active
/// Directory.
/// </summary>

public sealed class RevertImpersonator : IDisposable {
  public RevertImpersonator() {
     // acquire the identity of the current user (the user being impersonated)
     _userIdentity = WindowsIdentity.GetCurrent();

     // revert to the underlying process' identity
     // for ASP.NET applications that impersonate, this will be the identity of the IIS process
     // (e.g., the identity of the application pool which, by default, is NETWORK SERVICE).
     // NOTE: the NETWORK SERVICE account will access network resources as the MACHINE$ account, local resources as NT AUTHORITY\NETWORK SERVICE.

     RevertedIdentity = ( 0 != RevertToSelf() );
  }

  private readonly WindowsIdentity _userIdentity;

  
  [DllImport("advapi32.dll")]
  private static extern int RevertToSelf();

  ~RevertImpersonator() {
     restore();
  }


  /// <summary>
  /// Returns whether the user's identity was successfully reverted on initialization.
  /// </summary>
  public bool RevertedIdentity { get; private set; }


  public void Dispose() {
     GC.SuppressFinalize(this);
     restore();
  }


  private void restore() {
     // re-impersonate the user during cleanup
     if ( RevertedIdentity )
        _userIdentity.Impersonate();
  }
}

Effectively, this class allows you to encapsulate database calls thus:

using ( new RevertImpersonator() ) {
   // perform data access here
}

An important note is warranted.  As previously mentioned, using this class will revert the identity on the thread to the process's identity.  You can set your IIS Application Pool to use a domain account rather than the default NT AUTHORITY\NETWORK SERVICE.  Doing so will require that the domain user have access to the database in question.  If, however, you decide to use the default, you must be aware of a few items.  First, a local database will be accessed with the NT AUTHORITY\NETWORK SERVICE account as expected.  A remote database will be accessed with the MACHINE$ account - this is how the NT AUTHORITY\NETWORK SERVICE account is authenticated remotely.

Sunday, July 13, 2008 9:02:49 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, July 01, 2008

While we use TFS (Team Foundation Server) for all of our current development projects, we do maintain a SourceGear Vault server for most if not all of our legacy projects.  I've had nothing but a great experience with Vault over the past 3 or 4 years that I've used it.

Recently, however, I moved the server and SQL databases over to a new Windows Server 2008 x64 system with SQL Server 2005 SP2.  Everything seems to transition smoothly.  This particular SCC system does not get a lot of activity due to the nature of what data it maintains, but about a month after the upgrade one user suddenly couldn't access the system with authentication errors.  Likewise, I was unable to access several pages on the administrative Vault website.  The error reported was 'Object reference not set to an instance of an object.'

The next thing I did was open the Sql Profiler and watch the activity as it pertained to the Vault database (sgvault).  I noticed that it was attempting to execute a stored procedure by the name of spgettreestructure with the repository id and the transaction id.  Attempting to run the statement directly, I was presented with a SQL Server error:

Msg 0, Level 11, State 0, Line 0

A severe error occurred on the current command.  The results, if any, should be discarded.

Msg 0, Level 20, State 0, Line 0

A severe error occurred on the current command.  The results, if any, should be discarded.

This had me a little worried, but I opened up a ticket with SourceGear support.  After a few emails back and forth, a GoToMeeting, I was referred to Pinalkumar's blog page.  It turns out this was a bug in SQL Server that was fixed in the Cumulative update package 6 for SQL Server 2005 SP2.  Applying the patch (http://support.microsoft.com/default.aspx/kb/946608/LN/) fixed the issue :)

Tuesday, July 01, 2008 9:53:43 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, June 27, 2008

I was putting together a small application today that presents data in a TextBox.  Now while this isn't too uncommon :), the text gets added to the TextBox a line at a time via the TextBox's .AppendText() method.  I needed to do this repeatedly and in succession.  As it turns out, an unwelcome side effect of AppendText is that it scrolls the TextBox as the text is appended.  This resulted in the TextBox getting cleared and then 'wiping' down as the text was added.  Not only was this visually unappealing, but I wanted the insertion point to stay at the top of the contents.

To my knowledge, which is quite flawed and limited, there's not a built-in mechanism in .NET that provides this functionality.  Sure, I could override the TextBox control and manage the WndProc method and/or use SetStyle to make the control user drawn.  But I didn't want to do that.  Well, accomplishing this is quite easy.

There are a few ways we can do it:

  1. Use the LockWindowUpdate() API function.
  2. Use SendMessage() API function with the WM_SETREDRAW message.

LockWindowUpdate() is slick and arguably easier to use, but it's intended purpose isn't to suppress the redrawing of controls in this manner.  Plus, you can only have one window locked at a time.  So I threw that one out.

I wrapped the logic to lock and unlock the redrawing of the control in an IDisposable object so I wouldn't have to worry about remembering to clean up after myself.  My class is as follows:

public class LockVisualUpdate : IDisposable {
   public LockVisualUpdate(IWin32Window control) {
      _hWnd = control.Handle;
      SendMessage(control.Handle, WM_SETREDRAW, 0, 0);
   }


   private readonly IntPtr _hWnd;
   private const int WM_SETREDRAW = 0x000B;


   [DllImport("user32.dll")]
   private static extern bool SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);


   [DllImport("user32.dll")]
   private static extern bool InvalidateRect(IntPtr hWnd, IntPtr lpRect, bool bErase);


   public void Dispose() {
      SendMessage(_hWnd, WM_SETREDRAW, 1, 0);
      InvalidateRect(_hWnd, IntPtr.Zero, false);
   }
}

Really simple to use.  To consume it, I simply have to do the following:

using ( new LockVisualUpdate(textBox1) ) {
   textBox1.AppendText(...);
   // ...repeated as often as necessary to populate the control

   // when finished, position the insertion point to the top of the control
   textBox1.SelectionStart = 0;
   textBox1.ScrollToCaret();
}

.NET | C#
Friday, June 27, 2008 1:28:14 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Monday, June 23, 2008

A few days ago I realized that my blog wasn't working properly.  This came with more than a bit of frustration because I had upgraded ito to DasBlog on May 25th (almost 1 month ago exactly!).  What clued me off was the fact that I wasn't getting any comments to prior posts.  While the vast majority of comments came from my download control and my rating control (neither of which has yet been ported over), I would get the occasional comment through the blog directly.  But since the upgrade nothing.  Well, that's not true, I got two comments on my transition post but I didn't get either one because I didn't set up my mail setting correctly.  That has since been fixed.

As it turns out, the code that I originally used to port the blog site over was flawed, but I didn't realize it until it was too late.  Pretty much all of my google links where my blog was the top page or in the top few have all but disappeared :(.  I have, however, updated the code (which I present below) in the event there is any other poor soul out there needing to migrate from .Text to DasBlog.

Comments should now work on the blog.

I hope this works better for everyone moving forward:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using newtelligence.DasBlog.Runtime;

namespace ConvDotTextToDasBlog {
   internal class Program {
      private class EntryData {
         public EntryData(string id, string title) {
            Id = id;
            Title = title;
         }

         public readonly string Id, Title;
      }

      private static readonly Dictionary<int, EntryData> _postDict = new Dictionary<int, EntryData>();

 
     private static void Main() {
         string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "output");
         Directory.CreateDirectory(path);

         IBlogDataService dataService = BlogDataServiceFactory.GetService(path, null);
         string connStr = @"Initial Catalog=Blog; Data Source=(local)\SQLEXPRESS; Integrated Security=True";

         using ( SqlConnection conn = new SqlConnection(connStr) ) {
            conn.Open();
            using ( SqlCommand cmPosts = new SqlCommand("SELECT COUNT(ID) FROM blog_Content WHERE PostType=1; SELECT * FROM blog_Content WHERE PostType=1", conn) )
            using ( SqlDataReader drPosts = cmPosts.ExecuteReader() ) {
               drPosts.Read();
               int totalCount = drPosts.GetInt32(0);
               drPosts.NextResult();

               int currIndex = 0;
               while ( drPosts.Read() ) {
                  int postId = drPosts.GetInt32(0);
                  string postTitle = drPosts.IsDBNull(1) ? string.Empty : drPosts.GetString(1);
                  DateTime dtCreated = drPosts.GetDateTime(2);
                  DateTime dtModified = drPosts.GetDateTime(10);
                  string postText = drPosts.GetString(12);
                  string postAuthor = drPosts.GetString(5);

                  // catalog the id, assigning it a guid
                  string newPostId = Guid.NewGuid().ToString().ToLowerInvariant();
                  string newPostTitle = ( postTitle.Length > 0 ? postTitle : postText.Substring(0, Math.Min(20, postText.Length)) );
                  _postDict.Add(postId, new EntryData(newPostId, newPostTitle));

                  Console.WriteLine("Processing Post #{0} ({1} of {2})", postId, ++currIndex, totalCount);
                  Entry entry = new Entry();
                  entry.CreatedUtc = dtCreated;
                  entry.ModifiedUtc = dtModified;
                  entry.Title = newPostTitle;
                  entry.Content = postText;
                  entry.EntryId = newPostId;
                  entry.Categories = getPostCategories(postId, connStr);
                  entry.Author = postAuthor;
                  dataService.SaveEntry(entry);
               }
            }

            using ( SqlCommand cmComments = new SqlCommand("SELECT COUNT(ID) FROM blog_Content WHERE PostType=3; SELECT * FROM blog_Content WHERE PostType=3", conn) )
            using ( SqlDataReader drComments = cmComments.ExecuteReader() ) {
               drComments.Read();
               int totalCount = drComments.GetInt32(0);
               drComments.NextResult();

               int currIndex = 0;
               while ( drComments.Read() ) {
                  int commentId = drComments.GetInt32(0);
                  int refPostId = drComments.GetInt32(13);
                  DateTime dtCreated = drComments.GetDateTime(2);
                  string commentAuthorName = drComments.IsDBNull(5) ? string.Empty : drComments.GetString(5);
                  string commentAuthorIp = drComments.IsDBNull(7) ? string.Empty : drComments.GetString(7);
                  string commentAuthorUrl = drComments.IsDBNull(11) ? string.Empty : drComments.GetString(11);
                  string commentText = drComments.GetString(12);

                  EntryData refEntry;
                  if ( !_postDict.TryGetValue(refPostId, out refEntry) )
                     Console.WriteLine("Error processing comment #{0} ({1} of {2}); post {3} was not resolved.", commentId, ++currIndex, totalCount, refPostId);
                  else {
                     Console.WriteLine("Processing Comment #{0} ({1} of {2})", commentId, ++currIndex, totalCount);
                     Comment comment = new Comment();
                     comment.EntryId = Guid.NewGuid().ToString().ToLowerInvariant();
                     comment.CreatedUtc = dtCreated;
                     comment.ModifiedUtc = dtCreated;
                     comment.TargetEntryId = refEntry.Id;
                     comment.TargetTitle = refEntry.Title;
                     comment.Author = commentAuthorName;
                     comment.AuthorHomepage = commentAuthorUrl;
                     comment.AuthorIPAddress = commentAuthorIp;
                     comment.Content = commentText;
                     dataService.AddComment(comment);
                  }
               }
            }
         }
      }

      private static string getPostCategories(int postId, string connStr) {
         const string sql = "SELECT cat.Title FROM blog_Links AS links INNER JOIN blog_LinkCategories AS cat ON links.CategoryID = cat.CategoryID WHERE links.PostID = @PostID";

         List<string> categories = new List<string>();
         using ( SqlConnection cn = new SqlConnection(connStr) )
         using ( SqlCommand cm = new SqlCommand(sql, cn) ) {
            cn.Open();
            cm.Parameters.Add("@PostID", SqlDbType.Int).Value = postId;
            using ( SqlDataReader dr = cm.ExecuteReader(CommandBehavior.CloseConnection) ) {
               while ( dr.Read() ) {
                  string category = dr.GetString(0);
                  categories.Add(category);
               }
            }
         }
         return string.Join(";", categories.ToArray());
      }
   }
}

.NET | DasBlog | Journal
Monday, June 23, 2008 9:46:47 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Saturday, June 14, 2008

I was experiencing a most perplexing issue yesterday and the day prior that I *finally* resolved late last night.  In essence, I have a Windows Service that I wrote that hosts an adapted Cassini server which, in turn, hosts an ASP.NET website product running on the client's computer.  The issue was the classic case of 'it works on my machine'.

I could install my service, configure it, and run an ASP.NET website on my computer.  It was a beautiful and exciting thing to witness.  When I went to execute it on another computer, however, the service would start up perfectly but when I made the first request to the website the service would crash (immediately stop) and log an error to the system's Application event log.  The error was a FileNotFoundException.

After troubleshooting the obvious stuff for a bit (folder permissions (the Service runs as NETWORK SERVICE), executables in the proper places, comparing systems, etc), I resorted to more drastic means. :)  Actually, this is technique I frequently employ when troubleshooting issues loading assemblies.

You can troubleshoot Fusion binding issues by simply setting up some registry keys on the machine in question.  Fusion, in short, is the name given by Microsoft for their assembly loading and binding technology in .NET.  Because I don't have the .NET SDK or Visual Studio on the target machine in question, I had to set it up manually.  I added the following registry keys:

HKLM\SOFTWARE\Microsoft\Fusion\LogPath (string) with value pointing to my desired output folder (C:\(TEMP)\FusionLog).
HKLM\SOFTWARE\Microsoft\Fusion\LogFailures (DWORD) with a value of 1.

I restarted the service and made another web request to witness the failure for the umpteenth time.  Sure enough I got some output in my FusionLog folder:

*** Assembly Binder Log Entry  (6/13/2008 @ 9:18:40 PM) ***

The operation failed.
Bind result: hr = 0x80070002. The system cannot find the file specified.

Assembly manager loaded from:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll
Running under executable  C:\Program Files\Devstone\WebHostSvc\WebHostSvc.exe
--- A detailed error log follows.

=== Pre-bind state information ===
LOG: User = NT AUTHORITY\NETWORK SERVICE
LOG: DisplayName = WebHostSvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx
 (Fully-specified)
LOG: Appbase = file:///C:/Documents and Settings/All Users/Application Data/Devstone/Web/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = f9861834
Calling assembly : (Unknown).
===
LOG: This bind starts in default load context.
LOG: Using application configuration file: C:\Documents and Settings\All Users\Application Data\Devstone\Web\web.config
LOG: Using machine configuration file from C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\config\machine.config.
LOG: Post-policy reference: WebHostSvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx
LOG: GAC Lookup was unsuccessful.
ERR: No codebases found to download from.
ERR: Unrecoverable error occurred during pre-download check (hr = 0x80070002).

The thing that immeidately stood out was the fact that the Initial PrivatePath, Dynamic Base, and Cache Base were all NULL.  On my development machine these all referenced the /Web/bin folder.  Why wasn't it looking in the \bin folder?

At this point I began debugging the service, actually stepping into the .NET Framework code itself to see if I could figure out the issue.  Delving into the HttpRuntime.cs file I could see that the \bin folder gets appended to the app domain's private paths in the InitFusion() method, just as I had expected.  However, somehow the code wasn't getting that far.  Unfortunately, I couldn't step through some of the code leading up to this point so it was difficult to see exactly why it wasn't getting here.  Actually, in retrospect, I should have looked at the code a little longer - the answer was right in front of me.

I noticed from the fusion log that it was performing a GAC Lookup for the assembly.  Now the assembly isn't GAC-registered on my development machine but I was at my wit's end with the issue.  So I GAC'd it:

gacutil /i WebHostSvc.exe

Immediately upon the next request I got the error I was looking for in the browser:

The current identity (NT AUTHORITY\NETWORK SERVICE) does not have write access to 'C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files'.

Aha!  The one thing I didn't check but should have before.  Granting NETWORK SERVICE rights to that folder fixed the problem straight away.

I then removed it from the GAC and removed the Temporary ASP.NET Files that were created, restarted the service, and ran it again to verify that it was running properly.

At this point I went back to the source code and sure enough I saw the line I was looking for.  When initializing, it invokes a method called SetUpCodegenDirectory (which maps to the Temporary ASP.NET Files folder).  This method ensures that the caller has write access to it.

So the moral of the story: make sure that the user account that you're using for ASP.NET has write access to the Temporary ASP.NET Files folder and you'll be a much happier developer.

.NET | Web
Saturday, June 14, 2008 9:42:17 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback