Audit your Content Type Hierarchy

Shortly after you deploy a SharePoint site you probably start to wonder how your carefully designed content types are actually being used. And abused. More importantly I want to know if any of my feature based content types have been disconnected (unghosted) from the XML source file.

To that end I looked into extracting and visualizing the content type hierarchy to get the overview – and I found that it was already built by some nice chaps at codeplex (can be found here)! The community works like never before 😉

I changed it in a number of ways (in my mind improved it):

  • The original only showed the content types declared at the site collection root, now I use a completely different way of finding inherited content types through the SPContentTypeUsage classes, to be able to show derived content types in sub sites as well
  • Using the SPContentTypeUsage will also give you derived list content types, so they can be displayed as well. There might be a large number of lists, so you can switch the list content type display on and off. Default is off.

    Caveat: To figure out whether a given (list) content type is a direct descendent of another I have to open the actual list content type and check. Some lists are special (like _catalogs) and will be rendered slightly off in the hierarchy (hierarchy “missing” but the list content type location will be there)

  • Added my very own detection of the ghosting/unghosting status of any given content type. There are three states: ghosted (connected to xml source), unghosted (disconnected from xml source) and “DB only” (created through the UI or OM, not from a feature)

At the end of the day pretty much all of the code has been changed, while keeping the layout. I’m still grateful for the very nice first step provided and I will post this code back to the codeplex site. Hopefully the guys there will accept the modified code.
[Updated April 14: Patch posted now]

Detecting Ghosted/Unghosted State

I did a lot of digging to figure out how to get at the ghosting status of a content type. You can have a look in the wss content database (table “ContentTypes”) to see if your content types are disconnected or not (column “definition” is null – see my other blog on this). There’s even a dedicated stored procedure that will tell you the ghosting state of a given content type (named “proc_IsContentTypeGhosted”).

I really did not want to directly call stored procedures on the database, so I had a good long hard look at the properties available through the SPContentType class (and many others through reflector) and came up with the following conditions for detecting the state:

  • Is it a feature based content type, by checking “FeatureID != null”, if not mark it as “DB only”. Note that this is actually a private field, so I have to go the long way around and fetch it through reflection.
  • If it is feature based on the version > 0 then it has been unghosted. The version number seem to work well for this. It will always be 0 for ghosted content types and be incremented by one every time somebody modifies (and thereby unghost it). According to the xml schema you can specify this field in your content type definition, but it will (fortunately) not make it to the database so the detection seems sound.

    If you make new versions of the xml file, the server won’t really notice (see post here).

Disclaimer: This algorithm works well for me, seems reasonable sensible, but there might be an edge case that I’m not aware of.

The Result

Here is what it looks like:

conenttypehierarchy.png

The page is rather long – you are seeing about half of it. It’s also slow, but that’s hardly a performance issue you should care to fix 😉

The Code

You need to create a feature to deploy the page, the feature.xml I use:

<Feature xmlns="http://schemas.microsoft.com/sharepoint/" Id="2D9921AE-D263-4e2b-B4F7-ABDD6223C8B0"
   Scope="Site"
   Title="My Administration Utilities (Site collection)"
   Description="My adminsitration tools at the site collection level">

   <ElementManifests>
      <ElementManifest Location="ContentTypeHierarchy.xml" />
   </ElementManifests>
</Feature>

The referenced ContentTypeHierarchy.xml file (will create a link in the Gallery section of site settings):

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction Id="ContentTypeHierarchy"
   GroupId="Galleries"
   Location="Microsoft.SharePoint.SiteSettings"
   Sequence="0"
   Title="Site Content Types Hierarchy"
   Description="Displays a hierarchy of content types.">

   <UrlAction Url="_layouts/ContentTypeHierarchy.aspx"/>
</CustomAction>
</Elements>

And finally the magic page, ContentTypeHierarchy.aspx (deployed to the TEMPLATE\Layouts folder) (you can download it here, you’ll have to rename it back to aspx):

<%@ Page Language="C#" MasterPageFile="~/_layouts/application.master" %>

<%@ Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.SharePoint.WebControls" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Reflection" %>
<%@ Register TagPrefix="wssuc" TagName="ToolBar" Src="~/_controltemplates/ToolBar.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ToolBarButton" Src="~/_controltemplates/ToolBarButton.ascx" %>
<asp:Content ID="Content2" runat="server" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea">
	Content Types Hierarchy
</asp:Content>
<asp:Content ID="Content3" runat="server" ContentPlaceHolderID="PlaceHolderPageDescription">
	This page shows all Site Content Types and their hierarchical relationships
</asp:Content>
<asp:Content ID="Content4" runat="server" ContentPlaceHolderID="PlaceHolderMain">

	<table border="0" width="100%" cellspacing="0" cellpadding="0">
		<tr>
			<td id="mngfieldToobar">
				<wssuc:ToolBar id="onetidMngFieldTB" runat="server">
					<template_buttons>
					  <wssuc:ToolBarButton runat="server" Text="<%$Resources:wss,multipages_createbutton_text%>" id="idAddField" ToolTip="<%$Resources:wss,mngctype_create_alt%>" NavigateUrl="ctypenew.aspx" ImageUrl="/_layouts/images/newitem.gif" AccessKey="C" />

						<table cellpadding="1" cellspacing="0" border="0">
							<tr>
								<td class="ms-toolbar" nowrap style="padding:0px">
									<asp:CheckBox runat="server" CssClass="ms-toolbar" Text="Show lists" id="cbList" AutoPostBack="true" />
								</td>
							</tr>
						</table>                

						<table cellpadding="1" cellspacing="0" border="0">
							<tr>
								<td class="ms-toolbar" nowrap style="padding: 0px">
									<asp:CheckBox runat="server" CssClass="ms-toolbar"  Text="Show descriptions" id="cbDesc" AutoPostBack="true" Checked="true" />
								</td>
							</tr>
						</table>
					</template_buttons>
				</wssuc:ToolBar>
			</td>
		</tr>
	</table>
	<asp:PlaceHolder ID="phHierarchy" runat="server" />
</asp:Content>

<script runat="server">
    Hashtable contentTypesShown = new Hashtable();

    protected override void OnLoad(EventArgs e)
    {
		base.OnLoad(e);

		try {
			EnsureChildControls();
			cbDesc.CheckedChanged += new EventHandler(CheckedChanged);
			cbList.CheckedChanged += new EventHandler(CheckedChanged);
			BuildHierarchy();
		}
		catch (Exception x) {
			OutputHtml(x.Message);
		}
	}

	void CheckedChanged(object sender, EventArgs e) {
		BuildHierarchy();
	}

	public void BuildHierarchy() {
		EnsureChildControls();
		phHierarchy.Controls.Clear();
        contentTypesShown.Clear();
		SPSite site = SPControl.GetContextSite(Context);
		SPContentTypeCollection types = site.RootWeb.ContentTypes;

		SPContentTypeId id = types[0].Id;
		OutputHtml("<table style='font-size:10pt' border='0'" + " cellpadding='2' width='100%'><tr><td><ol>");
		ShowContentType(types[0]);
		OutputHtml("</ol></td></tr></table>");
	}

	public void OutputHtml(string html) {
		Literal l = new Literal();
		l.Text = html;
		phHierarchy.Controls.Add(l);
	}

	public void ShowContentType(SPContentType currenttype) {
		try {
			OutputHtml("<li><a class='ms-topnav'" +
				" href=\"" + currenttype.Scope.TrimEnd('/') +
				"/_layouts/ManageContentType.aspx?ctype=" +
				currenttype.Id.ToString() + "\">" + currenttype.Name +
				"</a>" + ExtraInfo(currenttype) +
				(cbDesc.Checked ? "<span class='ms-webpartpagedescription'>" + currenttype.Description + "</span>" : "") +
				"</li>");

			SPContentTypeId id = currenttype.Id;

			OutputHtml("<ol>");

			System.Collections.Generic.IList<SPContentTypeUsage> usages = SPContentTypeUsage.GetUsages(currenttype);

			int listCount = 0;
            foreach (SPContentTypeUsage usage in usages)
            {
                if (!contentTypesShown.ContainsKey(usage.Url + "|||" + usage.Id))
                {
                    contentTypesShown.Add(usage.Url + "|||" + usage.Id, true);

                    if (!usage.IsUrlToList)
                    {
                        using (SPWeb subSite = SPControl.GetContextSite(Context).OpenWeb(usage.Url))
                        {
                            SPContentType subType = subSite.ContentTypes[usage.Id];
                            if (subType.Parent.Id == id && subType.Parent.Id != subType.Id)
                            {
                                ShowContentType(subType);
                            }
                        }
                    }
                    else if (usage.IsUrlToList && cbList.Checked)
                    {
                        //1. Find web + list
                        //2. Open content type
                        listCount++;

                        Match m = Regex.Match(usage.Url, "^(?<web>.*?/)(Lists/)?(?<list>[^/]+)$");
                        if (m.Success)
                        {

                            try
                            {

                                using (SPWeb subSite = SPControl.GetContextSite(Context).OpenWeb(m.Groups["web"].Value))
                                {
                                    SPContentType subType = subSite.Lists[m.Groups["list"].Value].ContentTypes[usage.Id];
                                    if (subType.Parent.Id == id && subType.Parent.Id != subType.Id)
                                    {
                                        OutputHtml("<li>(List) <a class='ms-topnav'" +
                                            " href=\"" + m.Groups["web"] + "_layouts/ManageContentType.aspx?ctype=" +
                                            usage.Id.ToString() + "&list=" + subType.ParentList.ID.ToString() + "\">" + subType.Name + "</a> " + usage.Url + "</li>");
                                    }
                                }
                            }
                            catch (Exception e)
                            {
                                //exceptions occur for some of the "special" list, e.g. catalogs, then just list the list ;-)
                                OutputHtml("<li>(List) " + usage.Url + "</li>");
                            }
                        }
                    }
                }
            }

			OutputHtml("</ol>");
		}
		catch (Exception x) {
			OutputHtml("<b>Error:" + x.ToString() + "</b>");
		}

	}

	string ExtraInfo(SPContentType type) {
		string info = "";

		//Ghosted?
		// if the content type came from a feature and version > 0 then it's ghosted (as
		// far as I've been able to determine).
		// Version will not be greater than 0 for non-ghosted content types.
		// Alternative is to query DB directly (proc_IsContentTypeGhosted).

		//Need to find the featureId (if any) through reflection, since it's not a
		// public property)
		System.Reflection.PropertyInfo featureIdProp = type.GetType().GetProperty("FeatureId", BindingFlags.NonPublic | BindingFlags.Instance, null, typeof(Guid), Type.EmptyTypes, null);
		object featureId = featureIdProp.GetValue(type, null);

		if (type.Version > 0 && featureId != null && ((Guid)featureId) != Guid.Empty) {
			info += "Storage: <b>UNghosted (DB)</b>|";
		}
		else if (featureId != null && ((Guid)featureId) != Guid.Empty) {
			info += "Storage: Ghosted (Disk)|";
		}
		else {
			info += "Storage: DB only|";
		}

		if (type.ReadOnly) {
			info += "ReadOnly|";
		}
		if (type.Sealed) {
			info += "Sealed|";
		}
		info += "Scope:\"" + type.Scope + "\"|";

		info = info.TrimEnd('|').Replace("|", ", ");
		if (info.Length > 0) {
			info = " <i>(" + info + ")</i> ";
		}
		return info;
	}
</script>

Remember to reference the files in your solution manifest file and you should be good to go 🙂

Summary

It works and I’m very happy with it. It is an invaluable tool to debug all sorts of content type related problems. Big thanks to the guys at codeplex for letting me hit the ground running on this.

What’s not in it? Site columns. Didn’t find a good way for it and the need seems limited.

Finally understand that this is not really an audit in the normal SharePoint sense, so you should also consider to enable auditing on content type changes as well. Even when you have locked down security properly accidents do happen (only for your fellow admins of course).

About Søren Nielsen
Long time SharePoint Consultant.

16 Responses to Audit your Content Type Hierarchy

  1. Ben Abler says:

    Soren,

    Thanks for posting this, we are finding it valuable in troubleshooting our content types. We are experiencing an error however that we’re hoping you can assist with. After the Annoucment CType we receive the following error:

    Error:System.NullReferenceException: Object reference not set to an instance of an object. at ASP._layouts_contenttypehierarchy_aspx.ShowContentType(SPContentType currenttype) in c:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\ContentTypeHierarchy.aspx:line 117Error:System.NullReferenceException: Object reference not set to an instance of an object. at ASP._layouts_contenttypehierarchy_aspx.ShowContentType(SPContentType currenttype) in c:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\ContentTypeHierarchy.aspx:line 117Error:System.NullReferenceException: Object reference not set to an instance of an object. at ASP._layouts_contenttypehierarchy_aspx.ShowContentType(SPContentType currenttype) in c:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\ContentTypeHierarchy.aspx:line 117

    When looking at our CTypes with another analyzer tool we are relatively certain it is failing on the “Link” CType. Looking at the code it appears that it’s failing somewhere around setting SPContentType or ShowContentType method. If you have any tips on what to look into, I would appreciate it.

    Thanks

  2. Søren Nielsen says:

    Hi Ben

    Glad it helped you too.

    I have not seen the error you describe. However if it fails somewhere in the ShowContentType method, I would guess that it would be in the beginning (as you got no html output for that CType, right?)

    I would do two things:
    1. Check the line
    OutputHtml(“<a class=’ms-topnav'” …

    at the beginning of the method. I refer to “.scope.TrimEnd()”. Add a null check around for the scope, e.g. (currenttype.scope == null ? “” : currenttype.scope.TrimEnd )

    2. Add some more try/catch statements. Dissect the method in 2, one for the top (output of the current type) and one for the bottom (recurse). Make sure that your catch statement makes it easy to identify where it failed.

    Let me know if it helps.

  3. Søren Nielsen says:

    By the way, what is line 117 in your file? Obviously I cannot rely on my own line numbers here…

  4. Anders Rask says:

    Hi Søren,

    the problem here actually is that the subType is null in ShowContentType, so it fails when you touch .Parent. A simple null check fixed the problem for me:

    SPContentType subType = subSite.ContentTypes[usage.Id];

    if ( subType != null && subType.Parent.Id == id && subType.Parent.Id != subType.Id)
    {
    ShowContentType(subType);
    }

    Thanx for posting this code!

    It gives a good (but yes, sloooow! +2min) view of the List Content Types for each Site Content Type.

    A good idea perhaps to have a dropdown with Site Content Types to filter on, if you wanted List Content Types for a specific CT only…

    Anders

  5. Anders Rask says:

    Another question:

    since you defined UNGhosted as disconnected from XML and “DB only” as created by UI or API, how do your CTypes end up UNGhosted?

    Is it when they are created as features but later on modified by UI or API?

    regards
    Anders

  6. Anders: Yes, exactly.

    For the record: It doesn’t matter if you it through API/OM or UI.

  7. Ed says:

    Hi Søren,

    Great page! But i got still the question that Anders asked, how to ghost a contenttype back again?

  8. Anders and Ed: To reconnect your content types, read here: https://soerennielsen.wordpress.com/2007/09/08/convert-%e2%80%9dvirtual-content-types%e2%80%9d-to-physical/

    Warning: There is no supported way to do this. If you are not faint of hearth it’s not hard to do 😉

  9. sandrar says:

    Hi! I was surfing and found your blog post… nice! I love your blog. 🙂 Cheers! Sandra. R.

  10. Pingback: petergerritsen.nl » Blog Archive » Useful SharePoint classes

  11. Hey there, interesting web site, just wondering what antispam program you have on your site for filtering out junk websites since I get lots on my website.

  12. Uli says:

    is it possible to get this as compiled feature so i can just install it.

  13. Kumar Brajesh says:

    Hi,
    I was looking for the way to check how to check the customization status of the content Type and this artile exactly fit the purpose.

    But i am not abel to download ContentTypeHierarchy.aspx. Could you please send this copy to my mail or can you copy paste the whole code here. The current Code placed is not full.

  14. hi Kumar

    It seems that my old .gif rename trick has been caught.

    I’ve updated the link above.

    Download here: http://soerennielsen.googlecode.com/files/contenttypehierarchy.aspx

    • Kumar Brajesh says:

      Thanks a lot Soren. This will save me a week days of effort. I was lookign exactly for this.

  15. Pingback: Making Features that Deploy Content Types Compatible with the Content Type Hub « Nick Hadlee's Blog on SharePoint and Other Occasional Rants…

Leave a reply to Kumar Brajesh Cancel reply