SharePoint

FeatureReceiver For Applying Custom Web.Config Changes

As I was reading through my SharePoint feeds I came across a post that struck a cord with me.  The post is by Mike Stringfellow and is about updating sections of the web.config through a SPFeatureReceiver.  It struck a cord because I had developed something very similar last July to make it simpler to make changes to the web.config through deployment.  It was always on my TODO list to blog about it but I obviously never got around to it.  Thanks to the "reminder" from Mike, let’s see what I did!

(By the way, the purpose of this is not to steal any of Mike’s thunder.  Actually, it looks like his solution is built better than mine, so I’ll give credit where credit is due.  My hope is to provide some nuggets to the community with the hope that a public solution can be made available.  Mike, if you read this, perhaps we could put together a CodePlex project?)

I’ll point out two things worth mentioning, then I’ll let the code speak for itself as once you understand these two concepts the rest falls into place.

First is the use of the SPWebConfigModification class to manage the updates to the web.config file.  The class is flexible…it allows you to create any modification, then using xpath define where the modification lives in web.config.  So with a little string manipulation to create the entry and some xpath work to find the location in the config file, all that’s left is to determine if we’re performing an Add or Remove and then call the proper method on the SPWebApplication.WebConfigModifications.  You’ll see this illustrated in the UpdateWebConfig method in the code below.

Second is the use, and format, of a settings file which needs to be packaged with the Feature and defines the changes to be made to web.config.  This allows the developer to simply include a new settings file in their Feature and the FeatureReceiver pulls that in and reads the changes.  No code for the developer.  As you can see in the code, the file needs to be named WebConfigChanges.xml.  Here’s the XSD:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="WebConfigChanges" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="WebConfigChanges" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="WebConfigChange">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="Attributes" minOccurs="0" maxOccurs="1">
                <xs:complexType>
                  <xs:sequence>
                    <xs:element name="Attribute" minOccurs="0" maxOccurs="unbounded">
                      <xs:complexType>
                        <xs:attribute name="Name" type="xs:string" />
                        <xs:attribute name="Value" type="xs:string" />
                      </xs:complexType>
                    </xs:element>
                  </xs:sequence>
                </xs:complexType>
              </xs:element>
            </xs:sequence>
            <xs:attribute name="XPathLocation" type="xs:string" />
            <xs:attribute name="ElementName" type="xs:string" />
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>

 
Here’s some explanation on the file:
 
Name Description
WebConfigChanges A collection of WebConfigChange entries to be applied to the web.config file. The top element of the file.
WebConfigChange A single WebConfigChange to be applied to the web.config file.
XPathLocation Gives the XPath location to the element where the new entry should be inserted.
ElementName The name of the element to insert.
Attributes A collection of Attribute entries to be applied to the element being inserted.
Attribute A single Attribute to be applied to the element being inserted.
Attribute:Name The name of the attribute.
Attribute:Value The value of the attribute.
 
And an example which enters a new authorizedType entry for a new custom workflow activity:
 
<?xml version="1.0" encoding="utf-8" ?>
<WebConfigChanges>
  <WebConfigChange XPathLocation="configuration/System.Workflow.ComponentModel.WorkflowCompiler/authorizedTypes"
                     ElementName="authorizedType">
    <Attributes>
      <Attribute Name="Assembly" Value="Company.Moss.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4" />
      <Attribute Name="Namespace" Value="Company.Moss.Activities" />
      <Attribute Name="TypeName" Value="*" />
      <Attribute Name="Authorized" Value="True" />
    </Attributes>
  </WebConfigChange>
</WebConfigChanges>

Hopefully that all makes sense.  That foundation being set, I’ll let the code explain the rest.  If you have any questions or suggestions please leave a comment.

using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
 
namespace Company.Moss.FeatureReceiver
{
    class CustomFeatureReceiver : SPFeatureReceiver
    {
        protected SPFeatureReceiverProperties _properties;
 
        #region base overrides
        public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        {
            //throw new Exception("The method or operation is not implemented.");
        }
 
        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        {
            //throw new Exception("The method or operation is not implemented.");
        }
 
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            //This is where the WebConfigChanges file should be
            string fileLoc = properties.Definition.RootDirectory + "WebConfigChanges.xml";
 
            //Check to see if a WebConfigChanges.xml file exists. If yes, we have work to do
            if (System.IO.File.Exists(fileLoc))
            {
                //Grab the properties
                _properties = properties;
 
                this.ProcessChanges(fileLoc, false);
            }
        }
 
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            //This is where the WebConfigChanges file should be
            string fileLoc = properties.Definition.RootDirectory + "WebConfigChanges.xml";
 
            //Check to see if a WebConfigChanges.xml file exists. If yes, we have work to do
            if (System.IO.File.Exists(fileLoc))
            {
                //Grab the properties
                _properties = properties;
 
                this.ProcessChanges(fileLoc, true);
            }
        }
        #endregion
 
        private void ProcessChanges(string FileLocation, bool removeModification)
        {
            string xPathLocation;
            string elementName;
            Dictionary<string, string> attributes = new Dictionary<string, string>();
 
            using (XmlReader reader = XmlReader.Create(FileLocation))
            {
                //Loop through all of the changes
                while(reader.ReadToFollowing("WebConfigChange"))
                {
                    //Clean out any attributes from past iterations
                    attributes.Clear();
 
                    xPathLocation = reader.GetAttribute("XPathLocation");
                    elementName = reader.GetAttribute("ElementName");
                    
                    //Make sure we have at least a path and element
                    if (xPathLocation == null || elementName == null)
                        throw new Exception("WebConfigChange missing required XPathLocation or ElementName attributes");
 
                    //Get the Attributes to apply
                    if (reader.ReadToDescendant("Attribute"))
                    {
                        do
                        {
                            attributes.Add(reader.GetAttribute("Name"), reader.GetAttribute("Value"));
                        } while (reader.ReadToNextSibling("Attribute"));
                    }
 
                    //Do the update
                    UpdateWebConfig(xPathLocation, elementName, attributes, removeModification);
                } 
            }
        }
        
        private void UpdateWebConfig(string XPathLocation, string ElementName,  
            Dictionary<string, string> Attributes, bool removeModification)
        {
            try
            {
                SPWebApplication webApp = null;
 
                //Get the web app
                //First check if it was deployed to a Site Collection
                SPSiteCollection siteCol = _properties.Feature.Parent as SPSiteCollection;
                if (siteCol == null)
                {
                    //Check if it was deployed to a site
                    SPSite site = _properties.Feature.Parent as SPSite;
                    if (site == null)
                    {
                        //Check if it was deployed to a Site
                        SPWeb web = _properties.Feature.Parent as SPWeb;
                        if (web != null)
                            webApp = web.Site.WebApplication;
                    }
                    else
                        webApp = SPWebApplication.Lookup(new Uri(site.Url));
                }
                else
                    webApp = siteCol.WebApplication;
 
                if (webApp != null)
                {
                    SPWebConfigModification modification =
                        new SPWebConfigModification(ElementName + CreateAttributeString(Attributes), XPathLocation);
                        
                    modification.Owner = "Company.Moss.FeatureReceiver.CustomFeatureReceiver";
                    modification.Sequence = 0;
                    modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
                    modification.Value =
                         string.Format(CultureInfo.InvariantCulture,
                         CreateModificationValueString(ElementName, Attributes),
                         CreateModificationValueArgs(Attributes));
 
                    if (removeModification)
                        webApp.WebConfigModifications.Remove(modification);
                    else
                        webApp.WebConfigModifications.Add(modification);
 
                    SPFarm.Local.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
                }
                else
                    throw new ApplicationException("Could not locate a web application");
            }
            catch (Exception ex)
            {
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = "WebConfigFeature";
                el.WriteEntry(ex.Message);
            }
        }
 
        /// <summary>
        /// Accepts a dictionary object with all of the attributes for the web modification and
        /// creates a string representing the attribute values which can be used when creating
        /// the SPWebConfigModification object.
        /// </summary>
        /// <param name="Attributes"></param>
        /// <returns></returns>
        private string CreateAttributeString(Dictionary<string, string> Attributes)
        {
            //Create a string that looks like this (no line breaks):
            //[@Assembly="Company.Moss.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4"]
            //[@Namespace="Company.Moss.Activities"]
            //[@TypeName="*"][@Authorized="True"]
 
            string result = "";
 
            //Check if there are attributes
            if (Attributes.Count > 0)
            {
                foreach (KeyValuePair<string, string> kvp in Attributes)
                {
                    result += "[@" + kvp.Key + "="" + kvp.Value + ""]";
                }
            }
            
            return result;
        }
 
        private string CreateModificationValueString(string ElementName, Dictionary<string, string> Attributes)
        {
            //Create a string that looks like this:
            //"<authorizedType Assembly="{0}" Namespace="{1}" TypeName="{2}" Authorized="{3}"/>"
 
            string result = "<" + ElementName;
 
            //Check if there are attributes (Kind of silly if there aren't!)
            if (Attributes.Count > 0)
            {
                int i = 0;
                foreach (string key in Attributes.Keys)
                {
                    result += " " + key + "="{" + i.ToString() + "}"";
                    i++;
                }
            }
 
            result += " />";
 
            return result;
        }
 
        private object[] CreateModificationValueArgs(Dictionary<string, string> Attributes)
        {
            //Create an object that looks like this:
            //"Company.Moss.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4", "Company.Moss.Activities", "*", "True"
            object[] result = new object[Attributes.Count];
            
            int i = 0;
            foreach(string value in Attributes.Values)
            {
                result[i] = value;
                i++;
            }
 
            return result;
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *