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!

No comments: