mtelligent

View Original

Painless SharePoint Web Config Modifications with Custom Features

One of the challenging aspects of custom development when working with MOSS is dealing with changes needed for the Web.config file. If you do it manually, you risk MOSS blowing away your changes whenever it feels like, as certain administrative actions will do this if you’re not careful.

If you instead create a feature that uses the object model and SPWebConfigModification, you have to deal with several layers of quirkiness. For example, the “Name” property isn’t just descriptive, it’s an XPath Selector.  Another example is that any mistakes you make with your XPath selectors will cause the feature to fail, but not roll back your changes (the faulty web config mod is still in the collection), so you need to ensure that you manually write code that removes the modification.

But with the right approach, you can get around those limitations and use SPWebConfigModification to update web config files for a given web application and not have to worry about SharePoint blowing things away. The biggest problem of all is how tedious it is to create these modifications for each and every node you want to add to the Web.config. It makes it extremely painful to update the web config for complex configurations needed to support IoC containers, Enterprise Library blocks or other time saving libraries.

For example, to get this simple configuration for the Enterprise Library Exception and Logging blocks we would need to create 22 separate SPWebConfigModification objects, each with its own xpath selectors for specifying where in the config file each node goes:

<loggingConfiguration tracingEnabled="true"
    defaultCategory="General" logWarningsWhenNoCategoriesMatch="true">
    <listeners>
      <add source="Enterprise Library Logging" formatter="Text Formatter" log="Application" machineName="" listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.FormattedEventLogTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"traceOutputOptions="None" filter="All" />
    </listeners>
    <formatters>
      <add template="Timestamp: {timestamp}&#xD;&#xA;Message: {message}&#xD;&#xA;Category: {category}&#xD;&#xA;Priority: {priority}&#xD;&#xA;EventId: {eventid}&#xD;&#xA;Severity: {severity}&#xD;&#xA;Title:{title}&#xD;&#xA;Machine: {machine}&#xD;&#xA;Application Domain: {appDomain}&#xD;&#xA;Process Id: {processId}&#xD;&#xA;Process Name: {processName}&#xD;&#xA;Win32 Thread Id: {win32ThreadId}&#xD;&#xA;Thread Name: {threadName}&#xD;&#xA;Extended Properties: {dictionary({key} - {value}&#xD;&#xA;)}"        type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter, Microsoft.Practices.EnterpriseLibrary.Logging, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </formatters>
    <categorySources>
      <add switchValue="All">
        <listeners>
          <add />
        </listeners>
      </add>
    </categorySources>
    <specialSources>
      <allEvents switchValue="All" />
      <notProcessed switchValue="All" />
      <errors switchValue="All">
        <listeners>
          <add />
        </listeners>
      </errors>
    </specialSources>
  </loggingConfiguration>
  <exceptionHandling>
    <exceptionPolicies>
      <add>
        <exceptionTypes>
          <add
            postHandlingAction="None">
            <exceptionHandlers>
              <add logCategory="General" eventId="100" severity="Error" title="Enterprise Library Exception Handling" formatterType="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.TextExceptionFormatter, Microsoft.Practices.EnterpriseLibrary.ExceptionHandling, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" priority="0" useDefaultLogger="false"
               name="Logging Handler" />
            </exceptionHandlers>
          </add>
        </exceptionTypes>
      </add>
    </exceptionPolicies>
  </exceptionHandling>

I always had a love/hate relationship with this approach, and always thought of building something to make it easier. I ended up creating an abstract base class for “FeatureReceivers” that makes it easy to convert Xml Strings with configuration data in it, to a collection of SPWebConfigModification objects (with EnsureChildControls) that can be then applied to merge those configuration changes into the MOSS Web.config file.

Then to painlessly create a feature that updates the Web Config modification, inherit from the provided base class and use the following code, passing in your own config string, owner string, and collection of nodes to ignore.

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
  //Custom Function in the base class to get the Web App despite the feature scope
  SPWebApplication application = GetCurrentWebApplication(properties);
  if (application != null)
  {
    //Clean out any old ones first, in case we had an issue previously. Should also be called on
    //feature deactivate.
    RemoveWebConfigModificationsByOwner(application, SPWebConfigModificationOwner);

    //This will parse the string and convert to all spwebmods with EnsureChildNode as the type.
    List<SPWebConfigModification> mods = CreateModifications(ConfigXML, SPWebConfigModificationOwner, NodesToIgnore);
    AddWebConfigModifications(application, mods);
  }
}

The Nodes to ignore parameter is there to create a collection of strings that if the node matches on of them, the base class won't create a SPWebConfigModification for it. This is useful if you don't want to add an extra node for appSettings, or othe types of nodes which you know are already in the web config.

The base class itself, turns your configuration string into an XMLDocument and iterates through each node creating SPWebConfigModifications as long as the node name isn’t in the IgnoreCollection. Keep in mind, the WebConfigModifications are all of type “EnsureChildControls” which works very well for adding new nodes to the web config, but don’t work well if the config node already exists. For this type of requirement (for example updating the CustomErrors to off, you need a modification of type “EnsureAttribute” and will have to roll this on your own.)

I haven’t tested every situation, so I’m sure this code will need to be tweaked to meet specific needs, but it should give you a nice head start for creating a clean feature. For example, keep in mind for Xpath selectors, it will use the node name alone if there is one or less attributes, otherwise it will use the “Name” attribute if it exists, otherwise the first attribute found for the node.

Once you create your custom class, you’ll need to specify it as the receiver class in a custom feature xml that gets deployed via wsp or manually, and then activate it. I recommend giving the feature the Web Application scope, and creating separate features for each environment you want to deploy (dev, staging, production), this way you can integrate the stsadm call to activate the environment specific feature in your build scripts.

I know it’s a shameless plug, but I’ve packaged all the code (for the base class, sample feature xml and sample inherited class) into a Snip-It Pro import file. So if you don’t have it, you can install a thirty day trial here. Then you can download and import this Snippet Library (by creating a snippet collection, right clicking the folder and selecting “Import” and then navigating to the file). Each file provided is a parameterized snippet which will allow you to customize class names, namespaces and values in the Configure Snippet area (bottom of the docking bar) before drag/dropping into Visual Studio.

 > Download the code snippets. (Snip-It Pro snippet archive)