Thursday, July 8, 2010

ASP.NET – Convert Relative URL to Absolute

Having built a blogging engine into my proprietary content management system at work, one thing I have to deal with in terms of the blog is making sure that any blog posts with references to website content make use of a fully-qualified, absolute URLs when they are rendered in the RSS feed.

Here’s two extension methods that will convert relative URLs into absolute URLs which include the full host name and port (if applicable). The first method will take a single app-relative URL (~/path/to/foo) and fully-qualify it, while the second method accepts a block of HTML and uses regular expressions to match HTML attributes which contain root-relative URLs (/path/to/foo) and replaces them using the first method.

/// <summary>
/// Converts the provided app-relative path into an absolute Url containing the full host name
/// </summary>
/// <param name="relativeUrl">App-Relative path</param>
/// <returns>Provided relativeUrl parameter as fully qualified Url</returns>
/// <example>~/path/to/foo to http://www.web.com/path/to/foo</example>
public static string ToAbsoluteUrl(this string relativeUrl) {
    if (string.IsNullOrEmpty(relativeUrl))
        return relativeUrl;

    if (HttpContext.Current == null)
        return relativeUrl;

    if (relativeUrl.StartsWith("/"))
        relativeUrl = relativeUrl.Insert(0, "~");
    if (!relativeUrl.StartsWith("~/"))
        relativeUrl = relativeUrl.Insert(0, "~/");

    var url = HttpContext.Current.Request.Url;
    var port = url.Port != 80 ? (":" + url.Port) : String.Empty;

    return String.Format("{0}://{1}{2}{3}", url.Scheme, url.Host, port, VirtualPathUtility.ToAbsolute(relativeUrl));
}

/// <summary>
/// Converts all root-relatives Urls within the provided HTML string to absolute Urls
/// </summary>
/// <returns>Provided HTML parameter with fully qualified Urls substituted</returns>
/// <example>href="/path/to/foo.jpg" to href="http://www.web.com/path/to/foo.jpg"</example>
/// <example>@import url(/path/to/foo) to @import url(http://www.web.com/path/to/foo)</example>
public static string HtmlAppRelativeUrlsToAbsoluteUrls(this string html) {
    if (String.IsNullOrEmpty(html))
        return html;

    const string htmlPattern = "(?<attrib>\\shref|\\ssrc|\\sbackground)\\s*?=\\s*?"
                              + "(?<delim1>[\"'\\\\]{0,2})(?!#|http|ftp|mailto|javascript)"
                              + "/(?<url>[^\"'>\\\\]+)(?<delim2>[\"'\\\\]{0,2})";
    
    Regex htmlRegex = new Regex(htmlPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
    html = htmlRegex.Replace(html, m => htmlRegex.Replace(m.Value, "${attrib}=${delim1}" + ("~/" + m.Groups["url"].Value).ToAbsoluteUrl() + "${delim2}"));

    const string cssPattern = "@import\\s+?(url)*['\"(]{1,2}"
                              + "(?!http)\\s*/(?<url>[^\"')]+)['\")]{1,2}";

    Regex cssRegex = new Regex(cssPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
    html = cssRegex.Replace(html, m => cssRegex.Replace(m.Value, "@import url(" + ("~/" + m.Groups["url"].Value).ToAbsoluteUrl() + ")"));

    return html;
}

Sources

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.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>
Download the Full Source Code with Demo Here

Sources

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.

Tuesday, May 11, 2010

C# Truncate String

Something I find myself needing to do quite often is truncate a string to a maximum pre-determined length for display purposes. This is easily accomplished using the string.Substring(int, int) method, however this approach is limited by the fact that without checking the length of the string being truncated, the Substring() method will throw an exception if the second parameter passed to is exceeds the actual length of the string.

Nothing fancy, but here is an extension method to save the trouble of manually evaluating the string before displaying it:
public static string Truncate(this string str, int maxLength) 
{
    if (str == null) return null;
    return str.Length <= maxLength ? str : str.Substring(0, maxLength);
}
Simply call string.Truncate(int) and the returned string will be chopped accordingly.

Thursday, April 22, 2010

How to Make a Tag Cloud in ASP.NET MVC

As many of you are aware, tagging offers users a quick and easy way to filter website content by interesting topics or categories and a tag cloud acts as a visually asethetic way to display links to all of the available tags with their prevelance indicated by font-size. Lately tag clouds have become very popular and they can be found all over the web in many different formats, but the most common format usually looks something like this:

tagcloud Thanks, Phil Haacked. :)

In this tutorial I will show you how to make a simple tag cloud with 5 different size distributions based on the frequency of each tag. The code I have built is contained inside of an HtmlHelper which accepts a Dictionary<string, int> containing the tags and their occurrence counts in each pair and a Func<string, string> which is called on each tag to produce the link which will be displayed. Before we build the tag cloud, we’ll need to get the tags out of our database along with their number of occurrences. This code will obviously vary based on your tagging implementation, but will probably look something like this:

(from tag in Tags 
 group tag by tag.Name 
 into g 
 select g).ToDictionary(g => g.Key, g => g.Count());

Once we have the tag list in hand we need to find the mimimum and maximum tag counts and establish a distribution between the difference of the minmum and maximum values.

var min = tagList.Min(t => t.Value);
var max = tagList.Max(t => t.Value);
var dist = (max - min) / 3;

The code for this tag cloud supports a total of 5 different sizes, but if you require more you can increase the divisor of the distribution equation and update the CSS class assignment logic accordingly. With the minimum, maximum and distribution values calculated we can now loop through all of the tags and assign CSS classes to each one based on their position in the distribution.

var links = new StringBuilder();
foreach (var tag in tagsAndCounts) {
    string tagClass;
    
    if (tag.Value == max) {
        tagClass = "largest";
    }
    else if (tag.Value > (min + (dist * 2))) {
        tagClass = "large";
    }
    else if (tag.Value > (min + dist)) {
        tagClass = "medium";
    }
    else if (tag.Value == min) {
        tagClass = "smallest";
    }
    else {
        tagClass = "small";
    }

    links.AppendFormat("<a href=\"{0}\" title=\"{1}\" class=\"{2}\">{1}</a>{3}", 
                        urlExpression(tag.Key), tag.Key, tagClass, Environment.NewLine);
}

After running the above code we now have all of the tag links in a StringBuilder which we can push to the output. Notice in the final line of the foreach loop I am calling urlExpression(tag.Key) this is the Func<string, string> I mentioned above that is used to return the Url for the tag.

Now that we have all the pieces, here's the full code for the HtmlHelper:

public static MvcHtmlString TagCloud(this HtmlHelper html, IDictionary<string, int> tagsAndCounts, Func<string, string> urlExpression, object htmlAttributes) {
    if (tagsAndCounts == null || !tagsAndCounts.Any())
        return MvcHtmlString.Empty;
    
    var min = tagsAndCounts.Min(t => t.Value);
    var max = tagsAndCounts.Max(t => t.Value);
    var dist = (max - min) / 3;
    
    var links = new StringBuilder();
    foreach (var tag in tagsAndCounts) {
        string tagClass;
        
        if (tag.Value == max) {
            tagClass = "largest";
        }
        else if (tag.Value > (min + (dist * 2))) {
            tagClass = "large";
        }
        else if (tag.Value > (min + dist)) {
            tagClass = "medium";
        }
        else if (tag.Value == min) {
            tagClass = "smallest";
        }
        else {
            tagClass = "small";
        }

        links.AppendFormat("<a href=\"{0}\" title=\"{1}\" class=\"{2}\">{1}</a>{3}", 
                           urlExpression(tag.Key), tag.Key, tagClass, Environment.NewLine);
    }
    
    var div = new TagBuilder("div");
    div.MergeAttribute("class", "tag-cloud");
    div.InnerHtml = links.ToString();
    
    if (htmlAttributes != null) {
        div.MergeAttributes(new RouteValueDictionary(htmlAttributes), false);
    }
    
    return MvcHtmlString.Create(div.ToString());
}

And here's the appropriate CSS:

.tag-cloud { text-align: center; }
.tag-cloud a { white-space: nowrap; padding: 5px; display: inline-block; }
.tag-cloud a:hover { background-color: #E1E1E1;}
.tag-cloud .smallest { font-size: 75%;}
.tag-cloud .small { font-size: 100%; }
.tag-cloud .medium { font-size: 125%;}
.tag-cloud .large { font-size: 150%; }
.tag-cloud .largest { font-size: 200%; }

In order to use the above code simply place it in an extension method class, reference the CSS, and call it your view like so:

<%= Html.TagCloud(Model.Tags, t => Url.Tag(t), null) %>

Sources

  • The basis for this code comes from Pete Freitag's Blog in a similar post that discusses building a tag cloud with ColdFusion.

Tuesday, April 20, 2010

System.ComponentModel.DataAnnotations Validation Attributes – Using Error Message Resource Files

Just a quick-tip if you’ve been making use of the wonderful set of model validation attributes available in System.ComponentModel.DataAnnotations.

If you elect to use the ValidationAttribute’s ErrorMessageResourceName and ErrorMessageResourceType properties, make sure the resource file type you point to is marked with a Public access modifier and not the default value of Internal or you will find that all of your validation attributes will be silently ignored (including System.ComponentModel.DisplayNameAttribute, interestingly enough).

[DisplayName("Email")]
[Required(ErrorMessageResourceName = "EmailMissing", ErrorMessageResourceType = typeof(GeneralForms))]
public string Email { get; set; }
resourceaccessmodifier

Wednesday, February 17, 2010

ASP.NET MVC2 RC2 - Breaking Area Registration Change

With the release of ASP.NET MVC RC2 there is a breaking change in the way area registration works. As of RC2, all AreaRegistration types must be marked public, or they will be ignored by calls to AreaRegistration.RegisterAllAreas(). To me this seems rather arbitrary in nature, but I suspect the reasoning for the change stems from the fact that with the initial preview of MVC2 areas had to be implemented as separate projects. Thus, if the access modifier on the AreaRegistration type for a given Area project is not public, it would not be picked up by the call to RegisterAllAreas() in the main web project without violating access modifier constraints (private member reflection). So to reiterate, if you were originally doing something like this:
internal class AdminArea : AreaRegistration {}
You must change the access modifier to public in order for the area to be registered and added to the routes table.

Thursday, October 29, 2009

Multiple Web.config Management Utility

Something we all face on a regular basis is managing our Web.config files across multiple deployment environments each with their own specific set of options. The trivial, but repetitive task of managing multiple config files can become quite tedious during development and in order to take some of that stress away I have built some utilities into my own build process that automatically maintain my config files for me, allowing me to worry about more important things. Using a simple batch script which executes some basic NAnt commands I am able to rapidly generate multiple web.config files from a single template with unique values in each.

To do this, I make use of NAnt’s built-in file merging functionality which takes two files and substitutes values from one into specific locations in the other and produces a combined output. Applying this concept to creating web.config files, I have a single “web.config.format” file which looks like a normal configuration file, but contains ${Property.Name} markers in areas where a value with a given name should be substituted. I then create multiple “{deployment-type}.property” XML files which contain elements indicating each property name and its value. Finally, this is all tied together by a batch script which calls the appropriate merge commands on each file in a directory I specify.

default.build (NAnt Script)
<?xml version="1.0"?>
<project default="configMerge">
 <property name="destinationfile"
   value="web.config" overwrite="false" />
 <property name="propertyfile"
   value="invalid.file" overwrite="false" />
 <property name="sourcefile"
   value="web.format.config" overwrite="false" />

 <include buildfile="${propertyfile}" failonerror="false"
   unless="${string::contains(propertyfile, 'invalid.file')}" />

 <target name="configMerge">
   <copy file="${sourcefile}"
       tofile="${destinationfile}" overwrite="true">
     <filterchain>
       <expandproperties />
     </filterchain>
   </copy>
 </target>
</project>
Sample .Property File
<?xml version="1.0"?>
<project>
 <property name="compilationDebug" value="true" />
 <property name="customErrorsMode" value="Off" />
 <property name="smtpServer" value="mail.website.com" />
 <property name="smtpFrom" value="admin@website.com" />
 <property name="googleAnalyticsKey" value="UA-12345678-9" />
</project>
Sample Web.config.format File
<?xml version="1.0"?>
<configuration>
 <configSections>
   <sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
     <section name="Project.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false"/>
   </sectionGroup>
 </configSections>
 <appSettings>
   <add key="GoogleAnalyticsKey" value="${googleAnalyticsKey}" />
 </appSettings>
 <system.web>
   <customErrors mode="${customErrorsMode}" defaultRedirect="AppError.aspx">
     <error statusCode="404" redirect="404.aspx"/>
   </customErrors>
   <compilation debug="${compilationDebug}" />
 </system.web>
 <system.net>
   <mailSettings>
     <smtp from="${smtpFrom}" deliveryMethod="Network">
       <network host="${smtpServer}" port="25" defaultCredentials="true" />
     </smtp>
   </mailSettings>
 </system.net>
</configuration>
ConfigMerge.bat (NAnt script invoker)
@ECHO OFF
SET formatFile=%2
SET outputDir=%1

IF "%2"=="" SET formatFile=%~dp0web.config.format

ECHO USING: %formatFile%
ECHO OUTPUT DIR: %outputDir%
ECHO.
IF NOT EXIST "%formatFile%" (
   ECHO Could not locate format file. Please provide a valid filename.
) ELSE (

   FOR /f %%p in ('dir /b "%~dp0*.property"') DO (
       ECHO GENERATING: web.%%~np.config FROM: %%p
       START /B /WAIT "NAnt" "%~dp0nant\nant.exe" -buildfile:"%~dp0default.build" configMerge -q+ -nologo+ -D:sourcefile="%formatFile%" -D:propertyfile="%~dp0%%p" -D:destinationfile=%outputDir%\web.%%~np.config
   )
)

To further simplify things, I built an additional batch script that invokes ConfigMerge.bat with it's first argument- output directory. By calling BuildWebConfigs.bat ..\Web I can tell the NAnt script to output the generated config files in my website project's "Web" directory (and additionally replace Web.config with my Web.debug.config).

BuildWebConfigs.bat (ConfigMerge.bat invoker)
@ECHO OFF
call "%~dp0build\ConfigMerge.bat" ..\Web
copy "%~dp0Web\web.debug.config" "%~dp0Web\web.config" /y
echo.
echo ********* BuildWebConfigs Complete *********

In the interest of convenience I added a call to the second batch script into my website project's build events and included it as an external tool which I can easily invoke from Visual Studio's UI:

 

Since Visual Studio doesn't let you define per-configuration build events in C# projects (as it does in C++ projects- or so I'm told) I manually edited the CSProj file and added an instruction at the very bottom to call my batch script on all non-Debug builds:

Partial .CSProj File
<Project>
 ......
 .......
 ........
 <PropertyGroup Condition=" '$(ConfigurationName)' != 'Debug' ">
   <PostBuildEvent>
     call $(SolutionDir)BuildWebConfigs.bat
   </PostBuildEvent>
 </PropertyGroup>
</Project>

I have configured all of my solutions that use this utility to have BuildWebConfigs.bat and a folder called "build" in the $(SolutionDir). The "build" folder is laid out like so:

Folder Structure
------------------------------
+ nant
  - nant.exe
  - <related nant files>
- ConfigMerge.bat
- default.build
- {somename}.property
- {somename}.property
- {somename}.property
- web.config.format
------------------------------

I have also built a NAnt script which performs the same actions as the ConfigMerge batch script, but I find it runs significantly slower (about 4.5 seconds longer) than the batch because the NAnt script has to spawn additional instances of NAnt to perform the required actions. I've included it here in case you'd like to play with it:

Alternative NAnt-based NAnt script invoker
<project name="generate configs" default="generate ">
 <property name="destinationfile"   value="web.config" overwrite="false" />
 <property name="propertyfile"  value="invalid.file" overwrite="false" />
 <property name="sourcefile"   value="Web.format.config" overwrite="false" />

 <include buildfile="${propertyfile}"   failonerror="false"   unless="${string::contains(propertyfile, 'invalid.file')}" />

 <target name="configMerge">
   <echo message="GENERATING: ${path::get-file-name(destinationfile)} FROM: ${path::get-file-name(propertyfile)}." />
   <copy file="${sourcefile}" tofile="${destinationfile}" overwrite="true">
     <filterchain>
       <expandproperties />
     </filterchain>
   </copy>
 </target>

 <target name="generate ">
   <foreach item="File" property="file">
     <in>
       <items>
         <include name="*.property" />
       </items>
     </in>
     <do>
       <property name="propertyfile" value="${path::get-file-name(file)}" overwrite="true"/>
       <property name="destinationfile" value="web.${path::get-file-name-without-extension(propertyfile)}.config" overwrite="true"/>
       <property name="sourcefile" value="Web.format.config" overwrite="true"/>
       <echo message="Generating: ${destinationfile} from ${propertyfile}."/>

       <exec program="nant\nant">
         <arg value="configMerge"/>
         <arg value="-nologo+"/>
         <arg value="-q"/>
         <arg value="-D:sourcefile=${sourcefile}"/>
         <arg value="-D:propertyfile=${propertyfile}"/>
         <arg value="-D:destinationfile=${destinationfile}"/>
       </exec>
     </do>
   </foreach>
 </target>
</project>

Downloads:

Sources:

Tuesday, October 27, 2009

Shell Command - Delete Visual SourceSafe Files

Visual SourceSafe has a tendency to litter version-controlled directories with 3 types of project tracking files (*.scc, *.vssscc, *.vspscc) that may be undesirable when sharing a project with others. This simple "Delete SourceSafe Files" Shell command will quickly delete all instances of those files in a directory and its sub-directories. To use, run this registry script:
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSourceSafe]
@="Delete SourceSafe Files"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\DeleteSourceSafe\command]
@="cmd.exe /c \"TITLE Removing SourceSafe Files in %1 && COLOR 9A && del *.vssscc /s /f && del *.vspscc /s /f && del *.scc /s /f\""
I got this idea from Jon Galloway's blog post on creating a shell entry to remove SubVersion files. (Yes, the code is almost identical.) Note: This will *not* remove source-control bindings from the SLN file. That must be done manually by opening Visual Studio. If this script is run on a source-controlled solution and the solution is run VS will produce an error message regarding missing source-control files. Confirming the default action will permanently remove the binding from the SLN file. For the lazy: Download Zipped Registry File. (My host refused to serve a .reg file, sorry!)

Thursday, September 3, 2009

ASP.NET Web.config - Simple Website Settings Provider

While working on a new website today I felt compelled to make myself a simple, type-safe accessor for fields in my Web.config. Using some handy reflection I came up with the solution below. Upon a call to Loader.Initialize(), reflection is used to examine all properties for the AppSettingsKeyAttibute. If the attribute exists for a given property, the property is mapped to the specified key in the provided value collection.
public class WebsiteSettings
{
 [AppSettingsKey("Website.Name"), Required]
 public static string WebsiteName
 {
  get;
  private set;
 }

 [AppSettingsKey("Uploads.TempDirectory")]
 public static string UploadsTemporaryDirectory
 {
  get;
  private set;
 }

 [AppSettingsKey("Products.ImagesDirectory"), Required]
 public static string ProductImagesDirectory
 {
  get;
  private set;
 }

 public static class Loader
 {
  public static void Initialize(NameValueCollection appSettings)
  {
   if (appSettings == null)
    throw new ConfigurationErrorsException("AppSettings collection was null.");

   foreach (var property in typeof(WebsiteSettings).GetProperties())
   {
    if (property.IsDefined(typeof(AppSettingsKeyAttribute), false) && property.CanWrite)
    {
     var attribute = (AppSettingsKeyAttribute)property.GetCustomAttributes(typeof(AppSettingsKeyAttribute), false)[0];
     var value = appSettings[attribute.AppSettingsKey];

     if (value != null)
     {
      try
      {
       var changeType = Convert.ChangeType(value, property.PropertyType);
       property.SetValue(null, changeType, null);
      }
      catch (Exception ex)
      {
       if (HttpContext.Current != null)
        ErrorSignal.FromContext(HttpContext.Current).Raise(ex);
      }
     }
    }
   }

   if (!Validate())
    throw new ConfigurationErrorsException("One or more required Application Setting is missing or empty.");
  }

  private static bool Validate()
  {
   foreach (var property in typeof(WebsiteSettings).GetProperties())
   {
    if (property.IsDefined(typeof(RequiredAttribute), false) && property.CanRead)
    {
     var value = property.GetValue(null, null);
     if (value == null)
      return false;
     if (value is string && string.IsNullOrEmpty(value as string))
      return false;
    }
   }
   return true;
  }
 }

 [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
 private sealed class RequiredAttribute : Attribute { }

 [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
 private sealed class AppSettingsKeyAttribute : Attribute
 {
  public string AppSettingsKey { get; private set; }

  public AppSettingsKeyAttribute(string appSettingsKey)
  {
   this.AppSettingsKey = appSettingsKey;
  }
 }
}
Mapping a setting to a property is as simple as decorating the property with the AppSettingsKey attribute which takes a string containing the name of that property in the collection. If the specified key is found, the class will attempt to convert the value of the provided key to the type of the property referencing it. If the type conversion fails, no value is assigned. The rules checked as a result of the Required attribute is that the value is not null and, if it is a string, not empty. If you find yourself using this, you may wish to further customize this evaluation and the conversion failure handlers, depending on your situation of course. Personally, I am calling the Loader.Initialize() method in the Application_Start() method of Global.asax:
protected void Application_Start(object sender, EventArgs e) 
{
    WebsiteSettings.Loader.Initialize(WebConfigurationManager.AppSettings);
}
If you found this useful, let me know!