Monday, June 21, 2010

ASP.NET - Redirect Module

As developers we are all intimately aware of the implications of changing the URL of existing content in our applications which have already been indexed by search engines and users. It is never enough to simply change a URL and update the links because old pages, bookmarks, or search results may still point to the original URL. That being so, it is imperative to add an appropriate HTTP 301 redirect for the original URL when modifying the location of existing content.

In Apache-based web servers adding a 301 Redirect is as simple as adding a .htaccess file with the appropriate mod_rewrite instructions; unfortunately, this simplicity of configuration is unavailable on an IIS server and when an application requires a redirect be added we are forced to either edit the IIS settings explicitly or add the redirect to the application’s code. Ultimately, the former solution is ideal because it allows IIS to bypass the ASP.NET request engine and immediately serve the redirect, but for many of us, editing the IIS configuration is not an option due to the use of shared hosting environments. In ASP.NET code, issuing a 301 Redirect in your Global.asax looks something like this:

protected void Application_BeginRequest(object sender, EventArgs e) {
    var oldUrl = new Uri("http://www.website.com/path/to/content");
    var newUrl = new Uri("http://www.website.com/new/path/to/content");

    if(string.Equals(Request.Url.AbsolutePath, oldUrl.AbsolutePath, StringComparison.OrdinalIgnoreCase)) {
        Response.StatusCode = 301;
        Response.Status = "301 Moved Permanently";
        Response.RedirectLocation = newUrl;
        Response.End();
    }
}

While searching for a more maintainable solution I came across a blog post by Scott Hanselman which directed me to a HttpModule written by Fritz Onion. The module developed is elegant and effective because it uses a special section in the website's configuration file to define each redirect, thus minimizing maintenance challenges associated with adding and removing individual redirects. That being said, Fritz's solution is dated and uses some obsolete code so I have gone ahead and modernized it a bit. In addition to the original feature set, I have added support for matching the entire request URL.

Issuing redirects with the module is as easy as registering and adding the <redirections /> configuration section and then declaring each redirect rule.

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="redirections" type="RedirectModule.Configuration.RedirectionsSection, RedirectModule"/>
  </configSections>
  <redirections>
    <add targetUrl="^http://website.com" destinationUrl="http://www.website.com" ignoreCase="true" />
    <add targetUrl="^~/FalseTarget.aspx" destinationUrl="~/RealTarget.aspx" ignoreCase="true"/>
    <add targetUrl="^~/2ndFalseTarget.aspx" destinationUrl="~/RealTarget.aspx" permanent="true"/>
    <add targetUrl="^~/(Author|Category|Tool)([A-Za-z0\d]{8}-?[A-Za-z\d]{4}-?[A-Za-z\d]{4}-?[A-Za-z\d]{4}-?[A-Za-z\d]{12}).aspx$" destinationUrl="~/Pages/$1.aspx?$1=$2"/>
    <add targetUrl="^~/SomeDir/(.*).aspx\??(.*)" destinationUrl="~/Pages/$1/Default.aspx?$2"/>
  </redirections>
  <system.web>
    <httpModules>
      <add name="RedirectModule" type="RedirectModule.RedirectModule"/>
    </httpModules>
  </system.web>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="RedirectModule" type="RedirectModule.RedirectModule, RedirectModule"/>
    </modules>
  </system.webServer>
</configuration>

Sources

Update: 03/26/12

I've made a few minor tweaks in the module's code. The library now targets .NET 2.0 for maximum compatibility and the sample project works much better out of the box.

Tuesday, June 8, 2010

ASP.NET MVC Flags Enumeration Model Binder

The default model binder that ships with ASP.NET MVC 2 handles the large majority of databinding scenarios; however, one feature it does not support is combining a set of flags enumeration values into a single value. With the default model binder if you try to post a form with multiple enumeration values for a single model item, the returned model will contain only the last enumeration value provided by the form.

To solve this problem I created a FlagsEnumerationModelBinder to automatically handle the bitwise-or operation required to combine the enumeration values. Given a controller action that receives a model item of an enumeration type with the FlagsAttribute set, on BindModel() the FlagsEnumerationModelBinder will first check that there is more than one enumeration value in the request and then bitwise-or each individual value into a single byte value which is then returned in a call to Enum.Parse(Type, String).

public class FlagEnumerationModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
if (bindingContext == null) throw new ArgumentNullException("bindingContext");

if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) {
var values = GetValue<string[]>(bindingContext, bindingContext.ModelName);

if (values.Length > 1 && (bindingContext.ModelType.IsEnum && bindingContext.ModelType.IsDefined(typeof(FlagsAttribute), false))) {
long byteValue = 0;
foreach (var value in values.Where(v => Enum.IsDefined(bindingContext.ModelType, v))) {
byteValue |= (int)Enum.Parse(bindingContext.ModelType, value);
}

return Enum.Parse(bindingContext.ModelType, byteValue.ToString());
}
else {
return base.BindModel(controllerContext, bindingContext);
}
}

return base.BindModel(controllerContext, bindingContext);
}

private static T GetValue<T>(ModelBindingContext bindingContext, string key) {
if (bindingContext.ValueProvider.ContainsPrefix(key)) {
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(key);
if (valueResult != null) {
bindingContext.ModelState.SetModelValue(key, valueResult);
return (T)valueResult.ConvertTo(typeof(T));
}
}
return default(T);
}
}

The FlagsEnumerationModelBinder assumes that each enumeration value in the request has the same field name. The values can be in string or numeric form.

Careful about using the Html.CheckBox() extension to select your enumeration values in the form because the CheckBox and RadioButton helpers output an additional hidden value for each field that is sent with the request whether or not the field is checked. If you use the helpers you’ll end up with “true” and “false” values in the request which are [probably] not part of your enumeration.