ASP.Net MVC – unit testing UpdateModel()

For an ASP.Net MVC solution, I wanted to write some unit tests for a controller action method that made use of Controller.UpdateModel(), and was surprised by the paucity of information on the topic.  Whilst there is nothing revolutionary here, it may prove useful to someone.  The key to the test is managing to supply the values of a posted FORM to the action method, whilst the signature of the method itself takes only a single identifier, such as an int, as a parameter.  As we will see, this is achieved using a FormCollection and the controller’s ValueProvider property.

Defining the View and Controller

To provide some context for this example, we have a form that allows us to update a product.  A product has a name, colour and id.  The form might look something like:

<%using(Html.BeginForm("UpdateProduct"))
  {%>
  <p>
     <label for="Name">Product Name:</label>
     <%=Html.TextBox("Name")%>
  </p>
  <p>
     <label for="Colour">Product Colour:</label>
     <%=Html.TextBox("Colour")%>
  </p>
  <%=Html.Hidden("Id")%>
  <input type="submit" value="Update" />
<%}%>

 

Our controller is pretty simple: it has an IProductProvider that will allow us to retrieve products, and a action method called UpdateProduct that is the action invoked by the above form.

public class ProductController : Controller
{
   private readonly IProductProvider _productProvider;
 
   public ProductController(IProductProvider productProvider)
   {
      _productProvider = productProvider;
   }
 
   [AcceptVerbs(HttpVerbs.Post)]
   public ViewResult UpdateProduct(int id)
   {
      var product = _productProvider.GetExistingProduct(id);
      UpdateModel(product);
      return View(product);
   }
}

 

The UpdateProduct method finds the product that matches the supplied id, and then updates the model with the Name and Colour values submitted in the form.

For completeness, the IProductProvider and Product are defined as follows:

public interface IProductProvider   
 {
    Product GetExistingProduct(int id);
 }
 
 public class Product
 {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Colour { get; set; }
 }

Testing UpdateModel()

In order to test UpdateModel, the test is going to need an HttpContext.  I used Rhino Mocks to create stubs for HttpContextBase and the controller’s IProductProvider :

[TestClass]
public class ProductControllerTests
{
   // Stubs
   private HttpContextBase _stubHttpContext;
   private IProductProvider _stubProductProvider;
 
   [TestInitialize]
   public void Init()
   {
      _stubHttpContext = MockRepository.GenerateStub<HttpContextBase>();
      _stubProductProvider = MockRepository.GenerateStub<IProductProvider>();
   }
}

 

Finally, the crux of this blog post: creation of the AAA test:

  1. Create two instances of Product, one to be returned by our call to IProductProvider.GetExistingProduct and another containing the new details supplied in the FORM’s POST.
  2. Prepare the IProductProvider.GetExistingProduct so that, when called, it returns the existing product.
  3. Initialise the ProductController, supplying the HttpContext stub.

Now we come to the key of the whole test: setting up the controller with the FORM values that would usually be posted by the View.  This can be done by creating a FormCollection that contains our test data and converting this into a type that we can assign to the ValueProvider property on the controller:

controller.ValueProvider = new FormCollection(
            new NameValueCollection()
               {
                  {"Name", newProduct.Name}, 
                  {"Colour", newProduct.Colour}, 
                  {"Id", newProduct.Id.ToString()}
               }
            ).ToValueProvider();

 

All that is left to do is complete the Act and Assert.  The full test looks like this:

[TestMethod]
public void ProductControllerTest_UpdateProduct_Expect_ProductModelUpdated()
{
   /***************
   * Arrange.
   ***************/
 
   //Define a couple of instances of Product.
   var originalProduct = new Product() { Id = 2, Colour = "Red", Name = "Roger" };
   var newProduct = new Product() { Id = originalProduct.Id, Colour = "Green", Name = "George" };
 
   //When GetExistingProduct is called, return the originalProduct instance of Product.
   _stubProductProvider.Stub(p => p.GetExistingProduct(Arg<int>.Is.Anything))
      .Return(originalProduct);
 
   //Create an instance of the ProductController, using our HttpContext stub.
   var controller = new ProductController(_stubProductProvider);
   controller.ControllerContext = new ControllerContext(_stubHttpContext, new RouteData(), controller);
 
   //Provide the controller with the test values in lieu of a genuine POST of a FORM.
   //We want to simulate the user submitting the details we defined in newProduct.
   controller.ValueProvider = new FormCollection(
      new NameValueCollection()
         {
            {"Name", newProduct.Name}, 
            {"Colour", newProduct.Colour}, 
            {"Id", newProduct.Id.ToString()}
         }
      ).ToValueProvider();
 
   /***************
   * Act.
   ***************/
   ViewResult viewResult = controller.UpdateProduct(newProduct.Id);
 
   /***************
   * Assert.
   ***************/
   var actualModel = viewResult.ViewData.Model;
   Assert.IsInstanceOfType(actualModel, typeof(Product));
   Assert.AreEqual(newProduct.Colour, ((Product)actualModel).Colour);
   Assert.AreEqual(newProduct.Id, ((Product)actualModel).Id);
   Assert.AreEqual(newProduct.Name, ((Product)actualModel).Name);
}
ASP.Net MVC: Validation a on page with multiple forms

A common scenario in web development is the desire to have multiple forms on the same page, some with similar field names.  Those coming from an ASP.Net background may not have had to give this scenario much thought before as the use of validation groups and ASP.Net’s rendering of control names and ids make it easy to ensure that the validation messages are associated with the expected elements.  When using ASP.Net MVC there are a couple of simple tricks that can be used to ensure that validation errors highlight the correct fields and the appropriate ValidationSummary is used. 

An example of a view with multiple forms is a page that displays both logon and registration forms on the same page: both forms may require user name and password fields, such as the one shown below:

CleanLogonForm

The first stab at writing the mark-up might look something like:

<%using (Html.BeginForm("Logon", "Logon"))
  {%>
  <fieldset>
    <legend>Logon</legend>
      <%=Html.ValidationSummary() %>
      <label for="UserName">User Name:</label>
      <%=Html.TextBox("UserName") %>
      <label for="Password">Password:</label>
      <%=Html.Password("Password") %>
  </fieldset>
  <input type="submit" value="Logon" />
<%} %>
 
<%using (Html.BeginForm("Register", "Logon"))
  {%>
  <fieldset>
    <legend>Register</legend>
      <%=Html.ValidationSummary()%>
      <label for="UserName">User Name:</label>
      <%=Html.TextBox("UserName")%>
      <label for="Password">Password:</label>
      <%=Html.Password("Password")%>
 
      <label for="Forename">Forename:</label>
      <%=Html.Password("Forename")%>
      <label for="Surname">Surname:</label>
      <%=Html.Password("Surname")%>
      <label for="EmailAddress">Email Address:</label>
      <%=Html.Password("EmailAddress")%>
  </fieldset>
  <input type="submit" value="Register" />
<%} %>

The corresponding controller may be along the lines of:

[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Logon(CredentialsModel model)
{
   NameValueCollection validationErrors = _authenticationService.Logon(model);
 
   return CredentialsAreValid(validationErrors) ?
                                View("Home") :
                                View(model);
}
 
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Register(CredentialsModel model)
{
   NameValueCollection validationErrors = _authenticationService.Register(model);
 
   return CredentialsAreValid(validationErrors) ?
                                 View("Home") :
                                 View("Logon", model);
}
 
private bool CredentialsAreValid(NameValueCollection validationErrors)
{
   foreach (string fieldName in validationErrors)
   {
      ModelState.AddModelError(fieldName, HttpUtility.HtmlEncode(validationErrors[fieldName]));
   }
 
   return validationErrors.Count == 0;
}

Both calls to the _authenticationService return a NameValueCollection where the key is the name of the property (ie control) that failed validation and the value is the error message.  By way of example, the Logon method on the AuthenticationService test stub always returns sample user name and password validation errors within a NameValueCollection.

public NameValueCollection Logon(CredentialsModel model)
{
   return new NameValueCollection
   {
      {"UserName", "User name must be a minimum of 8 characters."},
      {"Password", "The password must contain both numbers and letters."}
   };
}

For completeness, the CredentialsModel is as follows:

public class CredentialsModel
{
   public string UserName { get; set; }
   public string Password { get; set; }
   public string Forename { get; set; }
   public string Surname { get; set; }
   public string EmailAddress { get; set; }
}

 

Note that we want to have the same names for both logon and password elements in the view as we wish to use the same CredentialsModel as the parameter of both the Register and Logon actions. 

When submitting valid data in either form, the correct data is passed to the correct action, and all appears well and good.  The shortcomings of this code so far only become apparent if one of the fields fail validation.  For example, if the user was to attempt to logon without entering a user name or password, the UserName and Password elements in both the logon and registration section are highlighted (as they have the same name) and both validation summaries display the error messages:

MultipleErrors

We have two problems:

  • Multiple elements with the same name;
  • Multiple validation summaries with no way of distinguishing between them.

 

View with multiple elements with the same name

We can deal with the problem of multiple elements with the same name (which we need to have so we can use the same model for both of our actions) by taking advantage of the way ASP.Net MVC’s DefaultModelBinder deals with prefixes, inferring the prefix from the method parameter name.  First, we prefix the element names in each form with a different prefix: in this case adding the logon and registration prefix as appropriate. Now the UserName element in the logon form has the name logon.UserName, whilst the UserName element in the registration form is now registration.UserName.  Our view now looks like this:

<%using (Html.BeginForm("Logon", "Logon"))
  {%>
  <fieldset>
    <legend>Logon</legend>
      <%=Html.ValidationSummary() %>
      <label for="logon_UserName">User Name:</label>
      <%=Html.TextBox("logon.UserName") %>
      <label for="logon_Password">Password:</label>
      <%=Html.Password("logon.Password")%>
  </fieldset>
  <input type="submit" value="Logon" />
<%} %>
 
<%using (Html.BeginForm("Register", "Logon"))
  {%>
  <fieldset>
    <legend>Register</legend>
      <%=Html.ValidationSummary()%>
      <label for="registration_UserName">User Name:</label>
      <%=Html.TextBox("registration.UserName")%>
      <label for="registration_Password">Password:</label>
      <%=Html.Password("registration.Password")%>
 
      <label for="registration_Forename">Forename:</label>
      <%=Html.Password("registration.Forename")%>
      <label for="registration_Surname">Surname:</label>
      <%=Html.Password("registration.Surname")%>
      <label for="registration_EmailAddress">Email Address:</label>
      <%=Html.Password("registration.EmailAddress")%>
  </fieldset>
  <input type="submit" value="Register" />
<%} %>

Note that whilst the name of the input elements (the TextBox and Password controls) use a full-stop (“.”) as the separator between the prefix and the rest of the name, the label needs to use an underscore (“_”) when identifying the id of the element with which the label is associated (via the for property), as this is the separator the ASP.Net MVC renders when it creates the id of the element.  The Action’s model parameter binds to the element’s name, whilst the HTML label identifies the element using its id.

We also need to update the parameter names in our actions so that the match our prefix:

[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Logon(CredentialsModel logon)
{
   NameValueCollection validationErrors = _authenticationService.Logon(logon);
 
   return CredentialsAreValid(validationErrors, "logon") ?
                                View("Home") :
                                View(logon);
}
 
[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Register(CredentialsModel registration)
{
   NameValueCollection validationErrors = _authenticationService.Register(registration);
 
   return CredentialsAreValid(validationErrors, "registration") ?
                                 View("Home") :
                                 View("Logon", registration);
}
 
private bool CredentialsAreValid(NameValueCollection validationErrors, string prefix)
{
   foreach (string fieldName in validationErrors)
   {
      //Concatenate the prefix with the model property name (as stored in the key) to identify
      //the name of the HTML element on the page.
      ModelState.AddModelError(prefix + "." + fieldName, HttpUtility.HtmlEncode(validationErrors[fieldName]));
   }
 
   return validationErrors.Count == 0;
}
   }

Our Logon action now takes a CredentialsModel parameter called logon, whilst the Register action now has a parameter called registration.  This on its own won’t solve our validation problem: we also updated our call to ModelState.AddModelError within the method CredentialsAreValid, concatenating the appropriate prefix to the field name, using the full-stop separator.

Should we wish to be more explicit, or wish to use a parameter name that differs from the prefix, we can identify the prefix in the Action’s signature using the Bind attribute:

public ViewResult Logon([Bind(Prefix="logon")]CredentialsModel model)

Now when we attempt to logon without supplying a user name or password, only the elements that have failed validation are highlighted:

ValidationSummaryIncorrect

However, we still have a problem with the ValidationSummary, which we will deal with next.

Multiple ValidationSummary controls contained within the same page

In order to host multiple validation summaries on the same form, and be able to distinguish between each one, we adapted the Html.MyValidationSummary extension described on stackoverflow.  In essence, we use ViewData to identify which ValidationSummary we wish to use to display our error messages.

First, create an extension to HtmlHelper called NamedValidationSummary, which takes a string parameter called name:

public static class HtmlExtensions
{
   public static readonly string FormNameKey = "FormName";
   public static string NamedValidationSummary(this HtmlHelper html, string name)
   {
      if (html.ViewData[FormNameKey] != null
         && !string.IsNullOrEmpty(html.ViewData[FormNameKey].ToString())
          && (html.ViewData[FormNameKey].ToString() == name))
      {
         return html.ValidationSummary();
      }
      return "";
   }
}

We then update our view to use the NamedValidationSummary, supplying the appropriate prefix for the associated form as the name parameter:

<%using (Html.BeginForm("Logon", "Logon"))
  {%>
  <fieldset>
    <legend>Logon</legend>
      <%=Html.NamedValidationSummary("logon") %>
      <label for="logon_UserName">User Name:</label>
      <%=Html.TextBox("logon.UserName") %>
      <label for="logon_Password">Password:</label>
      <%=Html.Password("logon.Password") %>
  </fieldset>
  <input type="submit" value="Logon" />
<%} %>
 
<%using (Html.BeginForm("Register", "Logon"))
  {%>
  <fieldset>
    <legend>Register</legend>
      <%=Html.NamedValidationSummary("registration")%>
      <label for="registration_UserName">User Name:</label>
      <%=Html.TextBox("registration.UserName")%>
      <label for="registration_Password">Password:</label>
      <%=Html.Password("registration.Password")%>
 
      <label for="registration_Forename">Forename:</label>
      <%=Html.Password("registration.Forename")%>
      <label for="registration_Surname">Surname:</label>
      <%=Html.Password("registration.Surname")%>
      <label for="registration_EmailAddress">Email Address:</label>
      <%=Html.Password("registration.EmailAddress")%>
  </fieldset>
  <input type="submit" value="Register" />
<%} %>

Finally, update the CredentialsAreValid method within the controller to set the ViewData item that is used by our NamedValidationSummary (in this case, the value of HtmlExtensions.FormNameKey which we defined as "FormName" within our HtmlExtensions class) to the name we gave the the relevant NamedValidationSummary in our view, which in this case is the same as the form’s prefix:

private bool CredentialsAreValid(NameValueCollection validationErrors, string prefix)
{
   foreach (string fieldName in validationErrors)
   {
      //Concatenate the prefix with the model property name (as stored in the key) to identify
      //the name of the HTML element on the page.
      ModelState.AddModelError(prefix + "." + fieldName, HttpUtility.HtmlEncode(validationErrors[fieldName]));
   }
   
   if(validationErrors.Count>0)
   {
      //We gave our NamedValidationSummary the same id as the prefix to the fields of the
      //form we are validating.
      ViewData[HtmlExtensions.FormNameKey] = prefix;
   }
 
   return validationErrors.Count == 0;
}

We have now managed to limit the displayed validation to the form that has been submitted:

WorkingValidation

IIS Search Engine Optimization Toolkit

I’ve spent a bit of time over the last few day’s playing with the IIS Search Engine Optimization Toolkit, whose workings are described on ScottGu’s Blog. In addition to providing all sorts of helpful SEO tips, it also proved to be a useful diagnostic tool, helping to identify bugs in the website such as broken links or poor HTML mark-up (such as pages containing two title elements as they were set in two places in the code).  It proved of particular use when dealing with the ASP.Net MVC Outgoing Routing ActionLink mismatch problem I was trying to resolve, highlighting the need for an HTTP 301 permanent redirect for some legacy URLs.

ASP.Net MVC Outgoing Routing ActionLink mismatch
 
We recently experienced an unexpected problem when attempting to re-route obsolete URLs using ASP.Net’s MVC routing mechanism.  The scenario was that we had been running a competition, and had a set of URLs associated with it in the format http://server/Competition/etc.  We now needed to re-route all incoming requests to these pages back to the site’s home page.  In order to achieve this, we added the following to the routing table:
 
routes.MapRoute(
   null,
   "Competition/{action}/{id}",
   new { controller = "Home", action = "Index", id = "" }
   );

 

In the majority of cases this worked fine.  However, a handful of links unrelated to the competition now exhibited strange behaviour.  For example, the menu item on the home page that was a link to itself (i.e. the home page) now had the URL http://server/Competition rather than http://server/.  The link itself was generated in the view Home/Index.aspx using an ActionLink:

<%= Html.ActionLink("Home", "Index", "Home")%>

 

It turned out that the problem was with self-referencing ActionLinks where the link matched the controller and action for the given view.  The outgoing URLs for these self-referencing ActionLinks now matched the new competition route.  The problem was resolved by adding a constraint to the route, ensuring that this route was only applied to incoming requests:

routes.MapRoute(
   null,
   "Competition/{action}/{id}",
   new { controller = "Home", action = "Index", id = "" },
   new { routeDirection = new IncomingRequestConstraint() }
);

 

The new constraint, IncomingRequestConstraint, checks the route direction:

public class IncomingRequestConstraint : IRouteConstraint
{
   public bool Match(System.Web.HttpContextBase httpContext, 
      Route route, 
      string parameterName, 
      RouteValueDictionary values, 
      RouteDirection routeDirection)
   {
      return routeDirection == RouteDirection.IncomingRequest;
   }
}

 

Post Script: Legacy URLs

The above example is a slightly simplified version of how we dealt with the obsolete URLs.  In order to establish that the re-routing was permanent and use an HTTP 301 redirect, we made use of the legacy URL routing described by Matt Hawley at eXcentrics World.  Our actual route looked like this:

routes.Add(
   null,
   new LegacyRoute
   (
      "Competition/{action}/{id}",
      "RedirectHome",
      new LegacyRouteHandler()
   )
);

 

As a result, the same outgoing URL now took the form http://server/Competition/Index/?controller=Home.  Adding the constraint resulted in:

 

routes.Add(
   null,
   new LegacyRoute
   (
      "Competition/{action}/{id}",
      "RedirectHome",
      new LegacyRouteHandler()
   )
   {
      Constraints = new RouteValueDictionary(
         new { routeDirection = new IncomingRequestConstraint() }
         )
   }
);
Configure SmtpClient to write to a file

 

When developing a .Net application that sends out an email it is sometimes useful to write the emails to a local directory rather than send them to a mail server.  This is easily done in the SmtpClient configuration in App.Config.  The below configuration section will write .eml files to the directory C:\Email (which you will need to create):

<configuration>
   <system.net>
      <mailSettings>
         <smtp deliveryMethod="SpecifiedPickupDirectory">
            <specifiedPickupDirectory pickupDirectoryLocation="C:\Email" />
         </smtp>
      </mailSettings>
   </system.net>
</configuration>

 

 

Note: if you are using Outlook 2007 you may not be able to open .eml files without an extra step.  The file can be opened from the command prompt with the command

"%ProgramFiles%\Microsoft Office\Office12\OUTLOOK.EXE" /eml "<PathToEmlFile>"

or associate the .eml files with a batch file that contains the text

"%ProgramFiles%\Microsoft Office\Office12\OUTLOOK.EXE" /eml %1

A fuller description can be found at http://support.microsoft.com/kb/956693.

Static content requires authentication in IIS 7

When working on an ASP.Net MVC application using Forms Authentication I found that the static content – .css, .jpg etc – was not being displayed for unauthenticated users.  Once the user was logged the static content was successfully retrieved.  As a result the logon page was, to put it politely, extremely plain.  What made this particularly frustrating was that the problem was experienced in my development environment, and that of one or two others, but not on all dev environments and also not on the production environment (which is a good thing…).  The dev environment is IIS7 running on Windows7 (although I suspect the same would be true of Vista machines); the prod environment uses IIS6.

Looking at Firefox’s error console I could see that the browser’s request for the css file was being redirected to the authentication page:

Error: The stylesheet http://localhost/Registration/Login?ReturnUrl=%2fContent%2fstylesheet.css was not loaded because its MIME type, "text/html", is not "text/css".
Source File:
http://localhost/Registration/Login?ReturnUrl=%2f
Line: 0

After a bit of digging I found that the problem was due to the permissions granted to the anonymous user identity in IIS7.

By default anonymous users in IIS7 run under the identity IUSR (rather than IIS6’s IUSR_MachineName).  This can be seen in the IIS Manager:

clip_image002

The default anonymous user is defined in a default installation in C:\Windows\System32\inetsrv\config\applicationHost.config

< anonymousAuthentication enabled = "true" userName = "IUSR" defaultLogonDomain = "" />

As the IUSR identity has very limited permissions, it did not have read access to the location of my website’s static content that I had placed in a directory of my choice (in this case D:\...\...\Website\Content) rather than <drive>:\inetpub\wwwroot.  In order to allow anonymous users to access the static content I had to grant IUSR read permission on my \Website\Content directory.

More info on IIS7 (and a comparison with IIS6) can be found at

http://learn.iis.net/page.aspx/140/understanding-the-built-in-user-and-group-accounts-in-iis-70/

http://learn.iis.net/page.aspx/110/changes-between-iis6-and-iis7-security

http://learn.iis.net/page.aspx/139/iis7-security-improvements/

Disable Windows Shut Down button on Start menu

 

In order to reduce the opportunity of inadvertently shutting down a remote Windows machine (which may then require a long drive to a remote data centre to switch it on again) it is possible to remove the Shut Down button from the Start menu and Windows Security dialog (ie the dialog displayed when you hit <ctrl><alt><delete>).

  1. From a command prompt or Run box, open the Group Policy Object Editor using the command:

    gpedit.msc

  2. Locate the setting Local Computer Policy –> User Configuration –> Administrative Templates –> Start Menu and Taskbar –> Remove and prevent access to the Shut Down command.
  3. Change the state to Enabled.
  4. Complete the update by running the following command at the command prompt:

    gpupdate /force

image

The machine can still be shutdown or restarted from the command prompt using the shutdown command.  Of course if you issue the wrong command at this point you may still have that long drive ahead of you...

To restart the machine:

shutdown /r

To restart instantly (by default there is a 30 second delay) indicating a planned shutdown for the reason "Reason":

shutdown /r /t 0 /d p:0:0 /c "Reason"

To shutdown the machine:

shutdown /s

Note that just typing shutdown (with no arguments) will display the help items rather than shutting down the machine.

Problems logging on to a Terminal Server

 

Something I have been asked about four times in the last week or so is how to log on to a remote server when the following message is displayed:

The terminal server has exceeded the maximum number of allowed connections.

image

There are two ways of dealing with this.

Both techniques use the command prompt. To connect to the machine from the command prompt, you need to run as an administrator identity on that machine. From the run box or a command prompt, open a new command prompt with:

runas /user:domain\administrator cmd

where domain\administrator is a user with administrator privileges on the target machine.

mstsc

First, try connecting to the console session. From the command prompt, type:

mstsc -v:0.0.0.0 /f -console

Where 0.0.0.0 is the IP address or server name. eg:

mstsc -v:172.21.2.3 /f -console

mstsc -v:machinename /f -console

To connect with an admin session:

mstsc -v:172.21.2.3 /f /admin -console

Once logged on to the session go to the Terminal Services Manager and logoff one of the other sessions. You then need to log off this session as, as a rule, you shouldn't log on to the console session as if the console session cannot be connected to as well you could be in all sorts of difficulty. Once logged off of the console session you can now connect to the freed RDP connection.

query

You can query and reset sessions from the command prompt:

  1. Find out which sessions are running:

    query session /server:machinename

  2. Reset one of the sessions:

    reset session 2 /server:machinename

Note that there is no feedback from the reset command, so a second query command will show you the new status.

NLB and HTTPS

I was recently looking a for some information on Microsoft's Network Load Balancing Service on Windows 2003 and the implications on the use of SSL.  Whilst Microsoft's TechNet documentation is comprehensive (http://technet.microsoft.com/en-us/library/cc738224%28WS.10%29.aspx), it does not provide the succinct overview that I required.

I found that Rick Strahl's article and walkthrough of the setup extremely useful (http://www.west-wind.com/presentations/loadbalancing/NetworkLoadBalancingWindows2003.asp).  Rick describes setting up single affinity port rules for HTTPS, but there is little discussion of the feasability and implications of load balancing HTTPS requests.

Précising information gleaned from TechNet:  SSL authenticated data uses client session state that is maintained across TCP connections.  Whilst successive HTTPS requests can be load balanced, as the session state is automatically recreated on each server by re-authenticatiing the client, this is expensive.  Single affinity is recommended.

From Microsoft's documentation:

Applications that use SSL with Single affinity are efficient because the SSL session IDs are reused. Negotiating a new SSL session ID requires five times the amount of overhead as reusing a SSL session ID. Although negotiating the SSL session ID is transparent to the client, the cumulative increase in overhead could degrade the performance of the cluster.

http://technet.microsoft.com/en-us/library/cc759039%28WS.10%29.aspx

Reporting Services AspNetSessionExpiredException
We recently had a problem viewing an SQL Server 2005 Reporting Services report via the ReportViewer within a web page.  The problem only occurred on one VM used for testing: there were no problems in the development or production environment.

The exception raised was:

Microsoft.Reporting.WebForms.AspNetSessionExpiredException: ASP.NET session has expired

After a bit of hunting around we found that modifying a timeout in the report server configuration resolved the problem.  We increased the DatabaseQueryTimeout within <drive>:\Program Files\Microsoft SQL Server\MSSQL.n\Reporting Services\ReportServer\rsreportserver.config to more than the default value of 120s.  Usual caveats about database timeouts apply.

<Configuration>
.
.
<Add Key="DatabaseQueryTimeout" Value="120"/>
.
.
</Configuration>

The key is described on MSDN at http://msdn.microsoft.com/en-us/library/ms157273.aspx.

Specifies the number of seconds after which a connection to the report server database times out. This value is passed to the System.Data.SQLClient.SQLCommand.CommandTimeout property. Valid values range from 0 to 2147483647. The default is 120. A value of 0 specifies an unlimited wait time and therefore is not recommended.