Disclaimer

The views expressed on this weblog are mine alone and do not necessarily reflect the views of my employer, Avanade.

Search
Recomends...
  • Code Complete, Second Edition
    Code Complete, Second Edition
    by Steve McConnell
Login
« Method get_XXX from assembly YYYY does not have an implementation | Main | Converting to Snip-It Pro's ".snip" format »
Saturday
Jan232010

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)

PrintView Printer Friendly Version

EmailEmail Article to Friend

Reader Comments (1)

This is a solution very similar to what I'm implementing myself, however, the problem is not so much in working with the XML snippets as in dealing with multiple zones. To date I have not seen anyone solve the issues with removing changes made via SPWebConfigModification from MULTIPLE ZONES. There is an SPUrlZone enum that I thought would have been taken into account by SPWebConfigModification, but alas, that was not taken into account. So regardless of how simple you make the CREATION of a group of SPWebConfigModification objects, you still have a problem with having to write your own manipulation if your site leverages any of the extended SPUrlZones beyond Default. This includes {Custom, Internet, Intranet and Extranet}.

I've seen some postings on manually writing stored proces to deal with the Content database, but that is not a solution for consulting folks looking to provide solutions for clients nor for people who want to keep their MOSS 2007 software supported by Microsoft. If you have any thoughts on how to resolve this problem without using direct XML manipulation to remove items from other SPUrlZones please let me know...great post otherwise and certainly using XML snippets instead of direct API manipulation is the way to go!
June 9, 2010 | Unregistered CommenterReynaldo Zabala

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
All HTML will be escaped. Hyperlinks will be created for URLs automatically.