Trying to make web.config modifications maintainable

UPDATE 09-11-15:
I updated the code to handle namespace uri:s and wrote a new post explaining those changes.

The other day I had to make some changes into a web.config file (in a SharePoint application). I had previously made a couple of changes into the same file so I thought to myself that this would be quite easy. The problem was, which I experienced, that I couldn´t very easy see how to insert this new alternation into my code. When I wrote the code five/six months ago I was into it and it seemed easy.
The code looked something like this: (though this is taken from the codeplex .net 3.5 feature)

OK, the code does the job and after while you see how to alter it but my thoughts went out to the new developer that comes into this application (and probably knows asp.net). How can that person effortlessly see how to alter a configuration file in SharePoint?
And also, let´s imagine that the new developer has to add a lot of configuration info for a couple of WCF services…bah

I decided to refactor my solution with the “new” developer in mind.

A new solution

I though a much more maintainable solution would be to include a web.config file in the actual feature, like you see in the screenshot above.

Note:
My initial thought was to completely bypass the WebConfigModification API in SharePoint and write some kind of custom SPJob that runs on all servers. After thinking it through I decided to not go down this path (partly because this is probably what the API already does and because it is probably unsupported).

If we look inside the “feature” config file this is what we´ll see:

That looks like a standard web.config file (this one actually includes the stuff needed for asp.net 3.5 and ajax control toolkit).
After activating the feature (on port 80) the web.config file in the virtual directory looks like this (partly):

Note: I have highlighted an area in both screenshots just to show that the changes have actually been applied.
This is the code in the feature event receiver that applies these changes (I will go into detail about the WebConfigManager class that you see a little later in the article):

using (WebConfigManager mgr = new WebConfigManager(properties))
{

mgr.Reporting += (sender, args) => log.Write(args.Value);
mgr.ApplyChanges();
mgr.Commit();

}

That looks pretty nice. I have created a new WebConfigManager (along with the SPFeatureReceiverProperties) and added a listener to the reporting event (for logging purposes).
Then I call the ApplyChanges() method and finally Commit(). The rest is up to WebConfigManager which we will come to later.
Note: I´ll go into detail about these methods later on

Duh, I forgot to add the “safecontrols” entry for the System.Web.Extensions.dll to my “feature” web.config file (that´s probably something the “new” developer might also do). Let´s apply that change and “re-activate” the feature.

The screenshot above shows the “feature” web.config file and the one below shows the “real” file.
And as you can see the that entry has now made its way into the “SharePoint” web.config file

One more problem

How do we handle changes that were made to attributes only (one example that comes to mind is the classic set debug configuration change). There are a couple of solutions to this but I have decided that these types of changes (rarely appliable I think) will have to live in their own feature.

Note: As a best practice I usually have all my web.config modifications in just one feature. This is because the API that writes these changes into SharePoint can sometimes be a bit buggy if you stress it to much, which is the case when using many smaller features that all write changes. Probably the cause of this is that the changes are written throughout the farm via SPJobs.

OK, the “smaller” web.config file that applies the debug attributes might look like this:

And the code for applying those changes in a feature event receiver look like this:

using (WebConfigManager mgr = new WebConfigManager(properties))
{

mgr.Reporting += (sender, args) => log.Write(args.Value);
mgr.ApplyChanges(true);
mgr.Commit();

}

The only change is that I will add a “true” flag into the ApplyChanges() method which tells the code to only look for attributes, not sections and childnodes.

OK, now let´s have a look at the code for WebConfigManager…

WebConfigManager

Let´s start by looking at the ApplyChanges() method

public void ApplyChanges(bool attributesOnly)
 {
 log.WriteLine("Applying all changes found in feature web.config file");

 if (attributesOnly)
 {
 attributemode = true;

 // Handle all attributes
 AddModifications(from node in FindConfigurationNodes()
                  where node.HasAttributes
                  select node,
                 SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute);
 }
 else
 {
 // Handle all sections
 AddModifications(from node in FindConfigurationNodes()
                  where node.HasAttributes == false
                  select node,
                 SPWebConfigModification.SPWebConfigModificationType.EnsureSection);

 // Handle all childnodes
 AddModifications(from node in FindConfigurationNodes()
                  where node.HasAttributes
                  select node,
                 SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode);
 }

 }

OK, so you can probably see that I use three different methods to find elements in the config file.

  • Attributes
  • Sections
  • Child Nodes

I use LinqToXml for this purpose and thus the constructor for the WebConfigManager has the ability to either send in an XDocument or the manager will find the web.config file in the root of the feature folder (with the name web.config) and parse that into an XDocument.
I have added a couple of different constructors to the WebConfigManager so you can instantiate it both from .exe´s and from features.

As you can see I make a call to a method called AddModifications, passing in the LINQ result (or actually the question since LINQ uses deffered execution). So let´s look at that method:

private void AddModifications(IEnumerable<XElement> elements, SPWebConfigModification.SPWebConfigModificationType modificationType)
 {

 log.WriteLine("Adding modifications of type '{0}'", modificationType.ToString());
 log.WriteLine("Found '{0}' results in feature web.config file", elements.Count());

 foreach (var item in elements)
 {

 SPWebConfigModification modification = new SPWebConfigModification
 {
 Path = BuildXPathFrom(item),
 Sequence = sequence++,
 Owner = owner,
 };

 switch (modificationType)
 {
 case SPWebConfigModification.SPWebConfigModificationType.EnsureSection:
 modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureSection;
 modification.Value = item.Name.LocalName;
 modification.Name = item.Name.LocalName;
 break;
 case SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode:
 modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
 modification.Value = item.ToString(SaveOptions.DisableFormatting);
 modification.Name = string.Format("{0}[@{1}='{2}']", item.Name.LocalName, item.FirstAttribute.Name.LocalName, item.FirstAttribute.Value);
 break;
 case SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute:
 modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureAttribute;
 modification.Value = item.FirstAttribute.Value;
 modification.Name = item.FirstAttribute.Name.LocalName;
 break;

 }

 if (application.WebConfigModifications.Any(Config => Config.Equals(modification)))
 {
 log.WriteLine("The modification '{0}' has already been added to the collection of modifications", modification.Name);
 continue;
 }

 log.WriteLine("Adding modication('{1}') '{0}' to the collection of modifications", modification.Name, modification.Type.ToString());
 application.WebConfigModifications.Add(modification);
 }
 }

OK, basically this method takes in a collection of nodes that the LINQ query finds in the web.config file and transforms them into the equivalent SPWebConfigModification entry.
Also the method checks that the modification doesn´t already exist, we wouldn´t want to add it again.

I have a little helper method in place there which assists with building the XPath expression. Let´s also have a look at that:

private string BuildXPathFrom(XElement node)
 {
 string from = node.Name.LocalName;
 var path = new List<string>();

 // build xpath
 while (node != null)
 {
 if (node.HasAttributes && attributemode == false)
 {
 path.Add(string.Format("{0}[@{1}='{2}']", node.Name.LocalName, node.FirstAttribute.Name.LocalName, node.FirstAttribute.Value));
 }
 else
 {
 path.Add(node.Name.LocalName);
 }
 node = node.Parent;
 }

 path.Reverse();

 if (attributemode == false && path.Count > 1)
 {
 path.RemoveAt(path.Count - 1);
 }

 string xpath = string.Join("/", path.ToArray());
 log.WriteLine("Building XPath from node '{0}' as '{1}'", from, xpath);

 if (from.Contains("remove"))
 {
    xpath =  string.Concat("/", xpath);
 }

 return xpath;
 }

Update 09-11-16: Found an issue with nodes like <remove verb=”*” path=”*.asmx” /> that have appear before others nodes.
Updated the code above to support that + the demo code. More on the topic can be found here (in the comments) or here.

I´m not going to show all the methods (I´ll supply the code instead) but Commit will just write the changes to SharePoint.

Lastly I´m going to mention a little utility method called FindAllModifications which has the following signature:

public string FindAllModications(Func<SPWebConfigModification, bool> match)

This makes it callable in many different ways but if you wan´t to get all the modifications call it like this:

and an example of the output is shown below:

One more things to think about:
Using the API to activate a feature with web.config modification can be a little buggy sometimes so if you have the ability to use the UI do so.
And… remember to use just one feature with all the modifications in it!

Here is a link to the code–>

, , , ,

  1. #1 by Five Elements on October 29, 2009 - 23:54

    At the top of the web.config file, there are new configuration sections that are registered that deal with AJAX. Five Elements

  1. Adding assemblies and namespaces to web.config to avoid duplication of code « Johan Leino

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: