Creating Custom Configuration Sections in ASP.NET

I recently created an application component to manage user lockouts in ASP.NET web applications. To configure it, I had several choices, including using a database table or using configuration settings in Web.config.

The service lets web applications monitor user lockouts for any number of actions. For example, I’ve used the monitoring logic to manage lockout conditions for login pages and for payment pages. It is easy to imagine that other applications could have other uses for lockout monitoring as well.

Key Technologies and Concepts

ASP.NET

C#

Custom Configuration Sections

Configuration Element Collections

ConfigurationSection

ConfigurationElement

ConfigurationProperty

ConfigurationElementCollection

Web.config

System.Configuration namespace

I knew that for at least one application I was planning to use it on, the customer would need to have control over some of the settings. If I put the settings into a database table, I’d need to provide a user interface because the customer is not SQL-savvy. The problem with creating a user interface was that I wanted the component to stand alone as an application "plug in," and having to install a separate web site for configuration was not a good option. In the past, the customer has had no problem with tweaking settings in global.asa or Web.config, so that seemed like the best way to go.

Defining the Configuration Requirements

Each action could have its own configuration requirements. For example, my login page might lock users out for 15 minutes after 3 failed attempts, but the payment page might lock users out for 24 hours. Additionally, the service itself needs its own configuration properties.

I decided that these configuration requirements would be best served by creating a custom configuration section in Web.config. The custom configuration section could have properties at the section level, and I could create a collection of configuration elements under it with one element per action.

My goal was to create a custom configuration section that looks something like this:

<LEI.LockoutService connection="MyDatabase">
   <actions>
      <add identifier="1" name="Payment" maxFailedAttempts="3"
         timeoutMinutes="1440" />
      <add identifier="2" name="Login" maxFailedAttempts="3"
         timeoutMinutes="15" />
   </actions>
</LEI.LockoutService>

The lockout service itself needs the name of a database connection string (the connection attribute) because it persists the monitoring data to a table in a SQL Server database.

Each action needed the following information:

  • identifier: An identifier that could be used in the database
  • name: A meaningful name that could be used in the application calls.
  • maxFailedAttempts: The maximum number of failed attempts allowed for each action.
  • timeoutMinutes: The lockout window for users who exceed the maximum attempts.

Creating a Simple Configuration Section

Creating a custom configuration section is pretty straightforward. However, creating the collection of custom elements gets a little complicated. As is often the case, the Microsoft documentation on the subject was disjointed, contrived, and confusing. After running in circles for a while, I finally extracted enough information to get the job done, but I had to "read between the lines" quite a bit to fully understand exactly what I was doing.

Hopefully, this article can save you some of that pain.

I’m going to start out by showing you how to create a very simple custom configuration section. I’ll build on that section and add the custom element collection once you see how the basics work.

Here are the basic steps to creating and using a custom configuration section:

  • Declare the custom section in Web.config with the characteristics you need.
  • Create a configuration class to process your custom section that inherits from System.Configuration.ConfigurationSection.
  • Add a section element to the configSections section in Web.config that wires up your configuration class to your custom section.
  • Use the WebConfigurationManager to initialize an instance of your configuration class from the settings in Web.config.

Declaring a Custom Section

As I mentioned, the lockout service needs the name of a database connection string. The idea is to configure the database connection in the connectionStrings section, and then reference the name of the appropriate connection string in the lockout service’s custom section.

Here’s an example of what the configuration would look like:

<?xml version="1.0"?>
<configuration>
…
   <connectionStrings>
      <add name="MyDatabase"
         connectionString="SQL Server connection string goes here"/>
   </connectionStrings>
 
   <LEI.LockoutService connection="MyDatabase"></LEI.LockoutService>
…
</configuration>

Creating a Configuration Section Class

ASP.NET loads the configuration settings you create in Web.config into corresponding objects within your application so you can access the settings. The System.Configuration namespace in the .NET Framework defines several configuration-related base classes that correspond to the elements you can set up in Web.config. For example, the namespace includes base classes for a ConfigurationSection, ConfigurationProperty, ConfigurationElement, and so on.

The class I need to create in order to read the minimal configuration section I have so far is pretty simple:

using System;
using System.Configuration;
 
namespace LEI.LockoutService.Configuration {
  internal class LockoutConfigSection : ConfigurationSection {
 
    [ConfigurationProperty("connection", IsRequired = true)]
    public String Connection {
      get { return (String)this["connection"]; }
      set { this["connection"] = value; }
    }
 
  }
}

I named my class LockoutConfigSection, and it inherits from the ConfigurationSection base class. You’ll notice that I chose internal scope for my class because I only want the class to be used within the lockout service assembly. You are welcome to make your class public instead if you need that level of access.

You have two choices for creating configuration properties. You can declare them using class properties adorned with attributes, or you can programmatically add them to a property collection. The Microsoft documentation refers to these approaches as the "declarative model" (or "attributed model") versus the "programmatic model," so don’t read too much into those terms if you run across them in the documentation.

Using attributes is by far the easiest way to go, so that’s what I show you in my sample code above. The ConfigurationProperty attribute tells ASP.NET that the Connection property corresponds to the connection attribute in the configuration file. Having the property name capitalized but the attribute name in camel case is not necessary, but it does seem to be a standard. The IsRequired parameter does exactly what it sounds like: it tells ASP.NET that the connection attribute must be present.

The ConfigurationProperty attribute may include other parameters that you’ll learn more about when I show you how to build a custom configuration element later in the article.

Wiring Up Your Configuration Class

Now that you have a custom section defined and you have a class to process it, you need to wire the two together. You do that by adding a declaration to the configSections section of the configuration file:

<configuration>
…
   <configSections>
      <section name="LEI.LockoutService"
         type="LEI.LockoutService.Configuration.LockoutConfigSection"
         requirePermission="false"
         allowDefinition="MachineToApplication"/>
   </configSections>
…
</configuration>

The critical attributes to note in the declaration are the name attribute, which must correspond exactly to the name of your custom configuration element, and the type attribute, which must correspond to the fully-qualified name of your configuration class. If necessary, you can include versioning information in the type specifier.

Loading Configuration Data at Run Time

The whole point of this exercise is to give your application access to the information that was entered into the Web.config file. To do that, you need to create an instance of your custom configuration class and initialize it with the data from the configuration file.

By inheriting the ConfigurationSection class and using ConfigurationProperty attributes, you make your custom configuration class conform to the ASP.NET configuration system. Consequently, you can let the WebConfigurationManager deserialize the configuration elements from Web.config and initialize an instance of your class automatically.

To perform this magic, use the WebConfigurationManager’s GetSection method, passing the name of the section you want to load:

LockoutConfigSection config;
config = (LockoutConfigSection)WebConfigurationManager.GetSection(
   "LEI.LockoutService");

That one line gives you an instance of your configuration class with all of the properties initialized from Web.config. For example, you could write code to retrieve the connection string name:

String connectionStringName = config.Connection;

In case you are wondering, yes you can create multiple sections in the Web.config file with different settings and use the same configuration class to process them. You would have to create multiple declarations in the configSections element. Each declaration would use the same type specifier, but it would reference different section names. You’d then use the different section names in your calls to GetSection.

Creating Custom Configuration Elements

Still with me? Good! This next part gets a little more complicated, but it builds on what you just learned.

So far, I have a basic custom section that tells my lockout service which database connection string to use. That’s a good start, but I still need a way to provide the configuration data for the various actions I want to monitor.

If the lockout service only supported specific actions, I could create custom elements and add them to the configuration like this:

<LEI.LockoutService connection="MyDatabase">
   <payment identifier="1"
      maxFailedAttempts="3" timeoutMinutes="1440" />
   <login identifier="2"
      maxFailedAttempts="3" timeoutMinutes="15" />
</LEI.LockoutService>

The property definitions for these elements in my configuration section class would then look like this:

[ConfigurationProperty("payment", IsRequired = true)]
public ActionElement Payment {
   get { return (ActionElement)base["payment"]; }
   set { base["payment"] = value; }
}
 
[ConfigurationProperty("login", IsRequired = true)]
public ActionElement Login {
   get { return (ActionElement)base["login"]; }
   set { base["login"] = value; }
}

ASP.NET can use reflection to determine that the payment and login elements map to the ActionElement class, because that is the type I used for my properties.

Here’s what the ActionElement class looks like:

internal class ActionElement : ConfigurationElement {
   public ActionElement() {
   }
 
   [ConfigurationProperty("identifier", DefaultValue = (int)1,
      IsRequired = true)]
   [IntegerValidator(MinValue = 1, MaxValue = 999999999,
      ExcludeRange = false)]
   public int Identifier {
      get { return (int)this["identifier"]; }
      set { this["identifier"] = value; }
   }
 
   [ConfigurationProperty("maxFailedAttempts", DefaultValue = (int)3,
      IsRequired = false)]
   [IntegerValidator(MinValue = 1, MaxValue = 99,
      ExcludeRange = false)]
   public int MaxFailedAttempts {
      get { return (int)this["maxFailedAttempts"]; }
      set { this["maxFailedAttempts"] = value; }
   }
 
   [ConfigurationProperty("timeoutMinutes", DefaultValue = (int)1440,
      IsRequired = false)]
   [IntegerValidator(MinValue = 1, MaxValue = 28800,
      ExcludeRange = false)]
   public int TimeoutMinutes {
      get { return (int)this["timeoutMinutes"]; }
      set { this["timeoutMinutes"] = value; }
   }
 
}

Once again, I’m using ConfigurationProperty attributes to map the class properties to their corresponding configuration file attributes. However, this time the class inherits from the ConfigurationElement base class, and the attributes are a little fancier than the simple Connection property I showed you earlier.

Here, I’m defining a default value for each property, and I added some validators to put constraints on the acceptable range of values for each property. If the values you read from the configuration file could end up in your database, you’ll want to do the same kind of thing.

This approach of using individual configuration elements to define the configuration of the payment and login actions works fine, as long as those are the only actions I want to support. If I wanted to support additional actions, I’d have to modify the code in my configuration section class to support the new elements.

But that’s not how I want this to work. If you recall, my requirements were to support any number of actions, not just payment and login. For that, I need an element collection instead.

Creating a Collection of Custom Configuration Elements

The ASP.NET configuration system does support collections of elements, but you have to use specific syntax in the configuration file to do it. The configuration system uses the "clear," "add," and "remove" keywords to work with items in the collection, which should be pretty familiar to you if you have ever used the appSettings or connectionStrings sections. Those sections are essentially custom configuration sections that contain a collection of custom configuration elements.

If you look back at my first configuration settings example (repeated below), you’ll see how I defined the actions as a collection.

<LEI.LockoutService connection="MyDatabase">
   <actions>
      <add identifier="1" name="Payment" maxFailedAttempts="3"
         timeoutMinutes="1440" />
      <add identifier="2" name="Login" maxFailedAttempts="3"
         timeoutMinutes="15" />
   </actions>
</LEI.LockoutService>

The first thing I need to do is modify my ActionElement class to include the name property, since the element name itself no longer provides that information.

internal class ActionElement : ConfigurationElement {
   public ActionElement() {
   }
 
   [ConfigurationProperty("name", DefaultValue = "Unknown",
      IsKey = true, IsRequired = true)]
   [StringValidator(MinLength = 1, MaxLength = 50)]
   public String Name {
      get { return (String)base["name"]; }
      set { base["name"] = value; }
   }
 
   // (other properties omitted for brevity)
 
}

Notice that the ConfigurationProperty attribute includes the IsKey parameter. I set it to true because I want to use the action name as the key of the collection. That will allow me to retrieve a specific item from the collection by name at run time.

Next I need a collection class to hold my ActionElement items. As you might have guessed, the configuration namespace includes a ConfigurationElementCollection base class. Your custom configuration element collection classes inherit from this class.

Not surprisingly, my class is called ActionElementCollection, and it looks like this:

internal class ActionElementCollection
      : ConfigurationElementCollection {
 
   public ActionElementCollection() {
   }
 
   public override ConfigurationElementCollectionType CollectionType {
      get { return
         ConfigurationElementCollectionType.AddRemoveClearMap; }
   }
 
   public ActionElement this[int index] {
      get { return (ActionElement)base.BaseGet(index); }
   }
 
   public new ActionElement this[string name] {
      get { return (ActionElement)base.BaseGet(name); }
   }
 
   protected override ConfigurationElement CreateNewElement() {
      return new ActionElement();
   }
 
   protected override object GetElementKey(
         ConfigurationElement element) {
      return ((ActionElement)element).Name;
   }
 
   public int IndexOf(ActionElement action) {
      return BaseIndexOf(action);
   }
 
   public void Add(ActionElement action) {
      BaseAdd(action);
   }
 
   public void Remove(ActionElement action) {
      if (BaseIndexOf(action) > 0) {
         BaseRemove(action.Name);
      }
   }
 
   public void RemoveAt(int index) {
      BaseRemoveAt(index);
   }
 
   public void Remove(string name) {
      BaseRemove(name);
   }
 
   public void Clear() {
      BaseClear();
   }
 
}

Okay, that looks like a lot of code, but really, most of what you are doing is overriding the properties and methods in the base class in order to provide the correct type definitions for each operation. For example, the CreateNewElement method needs to return an ActionElement instance.

A few things in the above code deserve special mention:

  • The CollectionType property specifies AddRemoveClearMap, which is what gives your collection the standard behavior of an element collection in the configuration file.
  • If you specify a key, you’ll need to override GetElementKey to return the value of the appropriate property. Note that your key property doesn’t have to be a string. I could have used my Identifier property as the key.
  • Some of the methods provide a familiar name to access corresponding base class operations. For example, the Clear method executes BaseClear.

The last thing I need to do is update my configuration section class to include a reference to the collection. Here’s what the final version of that class looks like:

internal class LockoutConfigSection : ConfigurationSection {
 
   [ConfigurationProperty("connection", IsKey = false,
         IsRequired = true)]
   public String Connection {
      get { return (String)base["connection"]; }
      set { base["connection"] = value; }
   }
 
   [ConfigurationProperty("actions", IsDefaultCollection = false)]
   public ActionElementCollection Actions {
      get { return (ActionElementCollection)base["actions"]; }
   }
 
}

The Actions property is defined as an ActionElementCollection, so reflection tells ASP.NET what kind of object to instantiate for the property. Interestingly, nothing explicitly tells ASP.NET what kind of class is in the collection. Presumably, the configuration system just calls CreateNewElement and lets the resulting class deserialize itself through the DeserializeElement method of the ConfigurationElement base class.

Accessing the Collection at Run Time

After all that coding, you can finally access your configuration properties from within your application. For example, if you want to retrieve the configuration data for the payment action, you might write something like this:

LockoutConfigSection config;
config = (LockoutConfigSection)WebConfigurationManager.GetSection(
   "LEI.LockoutService");
ActionElement action = config.Actions["Payment"];
int paymentId = action.Identifier;

Conclusion

The ASP.NET configuration system provides everything you need to extend the Web.config file to include your own application settings. Using the configuration system base classes, you can quickly implement complex configuration requirements without having to invent your own configuration system.