Initial setup for a new ASP.NET MVC site in IIS7

Background

Over the years, I have spent far too many hours running the same set of commands against ASP.NET and ASP.NET MVC sites to bring them up to what I consider a starting point. Most of the time, I have to refresh myself about how to do at least one of them. This is a list of those commands in a central location for myself and anyone else to use. As with any good instructions, there is no guarantee you won’t completely destroy your server, site, or soul using these commands. I can only say they worked for me once.

  1. Remove unnecessary HTTP headers
    1. Server
    2. X-Powered-By
    3. X-AspNet-Version
    4. X-AspNetMvc-Version
  2. Adding dynamic content compression (e.g., GZIP)
  3. GZIP for JSON and JSONP content types (e.g., “application/json”)
  4. Complete appcmd command-line list

Remove unnecessary HTTP headers

By default, IIS and ASP.NET give you a few headers in each and every HTTP response sent out.

Server: Microsoft-IIS/7.5 (or 7.0, or whatever IIS version you have)
X-Powered-By: ASP.NET
X-AspNet-Version: 4.0.303319 (or 2.0.50727, or 1.1.4322, or whatever version you have)
X-AspNetMvc-Version: 3.0 (or 2.0, or 1.0, or whatever version you have)

People will rant and rave about removing these headers for security and whether that helps. I am firmly on the side of “not-why-I-care-at-all”. I always figure if I am a target, obscurity doesn’t buy me anything when someone with very little computing power can just throw packets exploiting all the major frameworks at me and see which stick (which happens even when you aren’t a real “target”). The reason I do care is that removing these headers strips out even more data that has to be sent between you and the end user. Even though the size difference is small, it applies to every single HTTP response handled by your site. For such simple one-time changes, I will gladly take the savings.

Server (the annoying one)

This HTTP header appears to be the most annoying to remove. In fact, short of an HTTP module, the common method for doing so is a new line in a [probably new] event handler in global.asax.cs (or global.asax.vb).

protected void Application_PreSendRequestHeaders() {
    HttpContext.Current.Response.Headers.Remove("Server");
}

It’s not as pretty as the rest of the removal stuff below, but it gets the job done. In fact, they could all be solved this way, but I tend to lean on IIS to do the heavy lifting when I can.

X-Powered-By (the more annoying web.config/IIS one)

Many places that offer suggestions on removing the X-Powered-By: ASP.NET header talk about using IIS. You can certainly do this if you have permission to do so on your hosting provider and like the IIS GUI. I prefer things that I can push out with source control, so I go the web.config route.

<system.webServer>
    <httpProtocol>
        <customHeaders>
            <remove name="X-Powered-By" />
        </customHeaders>
    </httpProtocol>
</system.webServer>

Command line happiness:

%windir%\System32\inetsrv\appcmd.exe set config "{your-site-name}" /section:system.webServer/httpProtocol /-"customHeaders.[name='X-Powered-By']"

The root source of this setting is actually found in your server’s applicationHost.config (%windir%\system32\inetsrv\config\applicationHost.config). If you have the ability to modify that file, you can remove it there to make it the default for all IIS sites.

<system.webServer>
    <httpProtocol>
        <customHeaders>
            <clear />
            <add name="X-Powered-By" value="ASP.NET" /> <!--Remove this line.-->
        </customHeaders>
        <redirectHeaders>
            <clear />
        </redirectHeaders>
    </httpProtocol>
</system.webServer>

Command line happiness:

%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/httpProtocol /-"customHeaders.[name='X-Powered-By']" /commit:apphost

X-AspNet-Version (the easy web.config one)

To make X-AspNet-Version: {your-aspnet-version} go away, you just need to tweak your site’s web.config file. In the <system.web> section, under <httpRuntime>, which is probably already there, just make sure the enableVersionHeader="false" attribute is added to it.

<system.web>
    <httpRuntime enableVersionHeader="false" />
</system.web>

Command line happiness:

%windir%\System32\inetsrv\appcmd.exe set config "{your-site-name}" /section:system.web/httpRuntime /enableVersionHeader:false

I’m guessing this could be done at the applicationHost.config level, though I have never tried it (no real excuse, just too scared to mess with such a vital config entry too much).

X-AspNetMvc-Version (the global.asax code one)

Obviously, if you are tweaking a non-MVC ASP.NET site, you can ignore this one.

To make X-AspNetMvc-Version: {your-aspnetmvc-version} go away, you need to add some code and redeploy your site. In your global.asax.cs (or global.asax.vb) file, you will need to add a single line to the application start event.

protected void Application_Start() {
    MvcHandler.DisableMvcResponseHeader = true;
    // ...
}

Adding dynamic content compression (e.g., GZIP)

Reducing the amount of data you send over the wire is pretty good idea, especially if you are targeting low-bandwidth users like mobile browsers. While GZIP has some performance impact on the server and client, it is negligible on most systems. To add this system to IIS, add in the “Dynamic Content Compression” role service under the “Web Server (IIS)” role in “Server Manager”. If I am doing it visually, I typically just follow along with this iis.net article on the process.

Command line happiness:

%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/globalModules /+[name='DynamicCompressionModule',image='%windir%\System32\inetsrv\compdyn.dll'] /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/modules /+[name='DynamicCompressionModule',lockItem='true'] /commit:apphost

If this has already been installed, this will error out with a message about adding a duplication “DynamicCompressionModule” entry.

GZIP for JSON and JSONP content types (e.g., “application/json”)

[Update 2012-02-02] NOTE: If you have a load-balancer, you may need to figure out how to get it to allow these content types through as GZIP. In one situation where I added this setting to a series of servers behind F5′s BIG-IP load balancer, it still wouldn’t get GZIP out to the requester. BIG-IP was either stripping the request of Accept-Encoding: gzip or getting GZIP from IIS and decompressing it before letting it return to the original requester (I didn’t confirm what was happening, but I would assume the former since it is less CPU-intensive).

While getting GZIP compression is great for HTML, JavaScript, CSS, and most others with “Dynamic Content Compression” is great, it drops the ball on content types it doesn’t have explicit orders to manipulate. One such content type that should get compression is dynamically-generated JSON and JSONP.

While working on the [Sierra Trading Post API](http://dev.sierratradingpost.com/] I noticed that IIS with dynamic compression enabled doesn’t do anything to JSON content types, “application/json” (JSON) and “application/javascript” (JSONP). Compression is decided by the content types listed in applicationHost.config (%windir%\system32\inetsrv\config\applicationHost.config). By default, you get most of the types served by the average ASP.NET site: text/* (covering text/html). If you need to add more to this list, add them to the config file. It does not matter if they come before or after the <add mimeType="*/*" enabled="false" /> wildcard entry that is there by default.

<system.webServer>
    <httpCompression ... >
        ...
        <dynamicTypes>
            ...
            <add mimeType="application/json; charset=utf-8" enabled="true" />
            <add mimeType="application/javascript; charset=utf-8" enabled="true" />
        </dynamicTypes>
    </httpCompression>
</system.webServer>

Command line happiness:

%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/javascript; charset=utf-8',enabled='True']" /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/json; charset=utf-8',enabled='True']" /commit:apphost

For some reason, these are not recognized without the charset=utf-8 part. As well, if you don’t notice a change right away, that’s normal; you must recycle the app pool first (TechNet has a nice article with info about app pool recycling and why it doesn’t happen here).

 %windir%\System32\inetsrv\appcmd recycle apppool /apppool.name:"{your-app-pool-name}"

Again, some people would suggest an HTTP module for handling this, but it seems like IIS would be better equiped to handle this since it is already doing so for the rest of my outbound content.

Complete appcmd command-line happiness

If you are following along at home, most of this stuff can be poked into server config files, either applicationHost.config or your site’s web.config (all but the Server: Microsoft-IIS/7.5 HTTP header). If you want the fire-and-forget solution, here are all the commands smashed together for your batch file use.

%windir%\System32\inetsrv\appcmd.exe set config "{your-site-name}" /section:system.webServer/httpProtocol /-"customHeaders.[name='X-Powered-By']"
%windir%\System32\inetsrv\appcmd.exe set config "{your-site-name}" /section:system.web/httpRuntime /enableVersionHeader:false
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/globalModules /+[name='DynamicCompressionModule',image='%windir%\System32\inetsrv\compdyn.dll'] /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/modules /+[name='DynamicCompressionModule',lockItem='true'] /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/javascript; charset=utf-8',enabled='True']" /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/json; charset=utf-8',enabled='True']" /commit:apphost
%windir%\System32\inetsrv\appcmd recycle apppool /apppool.name:"{your-app-pool-name}"

If you want to do all this to applicationHost.config alone, you just swap out the first two lines from hitting “{your-site-name}” to pointing to the apphost config. Here they are all smashed together for that variant. NOTE: I have not confirmed that setting enableVersionHeader in applicationHost.config will work.

%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/httpProtocol /-"customHeaders.[name='X-Powered-By']" /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config /section:system.web/httpRuntime /enableVersionHeader:false /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/globalModules /+[name='DynamicCompressionModule',image='%windir%\System32\inetsrv\compdyn.dll'] /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/modules /+[name='DynamicCompressionModule',lockItem='true'] /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/javascript; charset=utf-8',enabled='True']" /commit:apphost
%windir%\System32\inetsrv\appcmd.exe set config -section:system.webServer/httpCompression /+"dynamicTypes.[mimeType='application/json; charset=utf-8',enabled='True']" /commit:apphost
%windir%\System32\inetsrv\appcmd recycle apppool /apppool.name:"{your-app-pool-name}"

Subtleties with using Url.RouteUrl to get fully-qualified URLs

At some point I missed the Url.RouteUrl overload that took a protocol and returned an absolute URL based on the current context. It is quite handy when you are sending URLs out into the world (e.g., RSS feed link). I ended up using the less-handy overload that took an explicit host (same as the current, in this case) and passing it in. When someone pointed out the simpler overload, I did the obvious and deleted the host from the call. That didn’t quite work.

For those looking for a way to get a fully-qualified URL through the route system, the less-than-obvious answer is to call the overload for Url.RouteUrl that gets a URL with a different protocol (Url.RouteUrl(string, object, string)), passing in Request.Url.Scheme for the current protocol.

Url.RouteUrl("Default", new { action = "Index", controller = "Department", id = 1 }, Request.Url.Scheme);
// http://www.currentdomain.com/department/index/1

Say you want to send someone to a different subdomain in your app while using the same routes. There’s an overload for that: Url.RouteUrl(string, RouteValueDictionary, string, string). Combined with the above example, here’s how these all play out if you are currently handling a www.currentdomain.com request and your route table includes the fallback default ({controller}/{action}/{id}).

Url.RouteUrl("Default", new { action = "Index", controller = "Department", id = 1 });
// /department/index/1
Url.RouteUrl("Default", new { action = "Index", controller = "Department", id = 1 }, Request.Url.Scheme);
// http://www.currentdomain.com/department/index/1
Url.RouteUrl("Default", new RouteValueDictionary(new { action = "Index", controller = "Department", id = 1 }), "http", "sub.currentdomain.com");
// http://sub.currentdomain.com/department/index/1

Now if you switch between the two fully-qualified calls, you may try just deleting or adding the hostName parameter, respectively. One direction is a compile error, and one direction is a runtime oddity resulting in a hideous URL.

Url.RouteUrl("Default", new { action = "Index", controller = "Department", id = 1 }, "http", "sub.currentdomain.com");
// Compile error (expects a RouteValueDictionary)
Url.RouteUrl("Default", new RouteValueDictionary(new { action = "Index", controller = "Department", id = 1 }), "http", "sub.currentdomain.com");
// Eye-bleeding and incorrect route as it "serializes" the RouteValueDictionary.
// In my case, I ended up with something like this:
// http://www.currentdomain.com/current/route/1/?Count=3&Keys=System.Collections.Generic.Dictionary%602%2BKeyCollection%5BSystem.String%2CSystem.Object%5D&Values=System.Collections.Generic.Dictionary%602%2BValueCollection%5BSystem.String%2CSystem.Object%5D

On a side note, if your development environment uses localhost with a port and you use some web.config app setting for that URL change between development and production (“localhost:12345″ vs “www.currentdomain.com”). You will want your host setting to be without the port. Url.RouteUrl will hiccup on your development environment if the port is part of the host name (it’s no longer just the host at that point).

Url.RouteUrl("Default", new RouteValueDictionary(new { action = "Index", controller = "Department", id = 1 }), "http", ConfigurationManager.AppSettings["hostwithport"]);
// http://localhost:12345:12345/department/index/1

Handling case-insensitive enum action method parameters in ASP.NET MVC

Skip to solution

Using enums as action method parameters

Say you have a enum, a sorting enum in this case.

public enum SortType {
    NewestFirst,
    OldestFirst,
    HighestRated,
    MostReviews
}

ASP.NET will gladly let you use that enum as an action method parameter; you can even make it an optional parameter. To make it optional by routing, you need to make it nullable for the action method function parameter in your controller and add some guarding logic (!sort.HasValue or the like).

routes.MapRoute(
    "DepartmentProducts",
    "Department/Products/{sort}",
    new { controller = "Department", action = "Products", sort = UrlParameter.Optional }
);
public ActionResult Products(SortType? sort) {
    SortType requestedSort = sort ?? SortType.NewestFirst;
    ...
}

To make the parameter optional by function parameter, just give the parameter a default value.

routes.MapRoute(
    "DepartmentProducts",
    "Department/Products/{sort}",
    new { controller = "Department", action = "Products" }
);
public ActionResult Products(SortType sort = SortType.NewestFirst) { ... }

Both work just fine, though I lean toward the function parameter default. Regardless of implementation, you can call the method by a number of different URLs:

  • http://www.somedomain.com/department/products/ (sort == null or SortType.NewestFirst, respectively)
  • http://www.somedomain.com/department/products/OldestFirst/
  • http://www.somedomain.com/department/products/HighestRated/
  • http://www.somedomain.com/department/products/?sort=MostReviews

Unfortunately, it requires the route value to be an exact match for the enum name, proper case included. This URL will not result in the correct value for the sort parameter.

http://www.somedomain.com/department/products/oldestfirst (results in null on the former, NewestFirst on the later)

This can be a problem with the internet being mostly case-insensitive, especially if you have a lower case routing system and/or use the lowercase SEO tweak in the IIS URL rewrite system (future post coming about a gotcha and work-around on applying this handy system to existing sites).

Solution

While looking into this, I came across just the code I thought I needed from Rupert Bates. It is case-insensitive model binder that is designed to allow you to declare a site-wide default for a given enum type. This was a great starting point. Since I tend to use the optional function parameters, though, this actually meant the model binder would give me a default before the action method could even try to do the same. I pulled that part from the version I used.

I tied the generic, as best as possible, to an enum (where T : struct) so it is harder to use it for types that aren’t compatible. While I was poking around, I flipped it to use a once-built look-up table for the enum using a case-insensitive Dictionary<string, T> rather than repeat calls to Enum.Parse (called once per value) and Enum.GetNames (called only once). As far as my manual testing has shown me, it works quite well and should be at least slightly faster, not that Rupert’s solution couldn’t hold its own just fine.

using System;
using System.Web.Mvc;
using System.Collections.Generic;

namespace StpWeb.CustomModelBinders {
    public class EnumBinderIgnoreCase<T> : IModelBinder where T : struct {
        public EnumBinderIgnoreCase() {
            foreach (string enumName in enumNames) {
                enumLookups.Add(enumName, (T)Enum.Parse(typeof(T), enumName, true));
            }
        }
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
            return bindingContext.ValueProvider.GetValue(bindingContext.ModelName) == null
                ? (T?)null
                : GetEnumValue(bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue);
        }

        private string[] enumNames = Enum.GetNames(typeof(T));
        private Dictionary<string, T> enumLookups = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
        private T GetEnumValue(string value) {
            T foundEnumValue = default(T);
            if (!String.IsNullOrEmpty(value) && enumLookups.ContainsKey(value))
                foundEnumValue = enumLookups[value];

            return foundEnumValue;
        }
    }
}