I've moved my blog to!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Friday, January 30, 2009

Don’t Use the PrepareToMove STSADM Command

Todd Carter just published an interesting post about when to use, and more appropriately, not use the preparetomove command:  Essentially if you have at least the Infrastructure Update (IU) installed then using this command will cause you issues.  See Todd’s post for more details.

Thursday, January 29, 2009

SPDisposeCheck Released

This is a must have!  If you do any kind of SharePoint development then you should strongly consider downloading the recently released SPDisposeCheck tool:  This tool analyzes your assemblies and helps to identify potential memory leaks.  I should note that you need to truly evaluate each result because not every item should be considered a show stopper – you need to understand the various dispose patterns and should use this tool as a quick way to check for things that your (or your peers) might have missed.

And, in case anyone is wondering – yes, I have run this tool against my extensions and here are the results:


Total Found: 0


Modules Checked: 1

Modules Ignored: 0

Methods Ignored: 0

Sometimes I know what I’m doing :)

Running it against my PowerShell cmdlets results in 6 issues found but they are all false positives due to the fact that I’m returning an SPSite and SPWeb object via a base property/method and I’m explicitly expecting the caller to dispose of these objects.  Here’s an example of that output:

ID: SPDisposeCheckID_110
Module: Lapointe.SharePoint.PowerShell.Commands.dll
Method: Lapointe.SharePoint.PowerShell.Commands.Proxies.SPSiteInfo.GetSPObject
Statement: CS$1$0000 := new Microsoft.SharePoint.SPSite(this.{Lapointe.SharePoint.PowerShell.Commands.Proxies.SPSiteInfo} get_ID())
Source: W:\work\Lapointe.SharePoint.PowerShell.Commands\Lapointe.SharePoint.PowerShell.Commands\Proxies\SPSiteInfo.cs
Line: 98
Notes:   NOTE: This instance was returned from the method and can likely be ignored as long as the type is disposed in the caller.
         SPDisposeCheck will automatically determine if the instance was disposed in the calling method
         Disposable type not disposed: Microsoft.SharePoint.SPSite
         ***This may be a false positive depending on how the type was created or if it is disposed outside the current scope
More Information:

If you have similar false positives in your code you can have the tool ignore these by adding the SPDisposeCheckIgnore attribute to your method or property.  Here’s a snippet from the documentation included with the tool:

Accepting an Issue
If you have investigated a reported issue and are satisfied that it doesn’t represent a problem you can have it ignored by the tool. To do this add the declaration of the SPDisposeCheckIgnore attribute to the method where the error is shown and specify the error to ignore. You also need to add the declaration for SPDisposeCheckIgnore to your project. You can safely change the namespace of SPDisposeCheckIgnore to match your project to avoid additional using statements in your source files. Here is an example, which you can see in the SPDisposeExamples project.

[SPDisposeCheckIgnore(SPDisposeCheckID.SPDisposeCheckID_110, "Don't want to do it")]

Tuesday, January 20, 2009

Working with SPSite(Info) Objects Using PowerShell

One of the first PowerShell cmdlets I built, Get-SPSite, addresses some common issues found with working with SPSite objects.  I struggled with how I could provide a means to quickly and easily get SPSite objects while at the same time helping administrators so they don’t have to worry (as much) about object disposal.  For those that aren’t familiar with the SPSite object (Microsoft.SharePoint.SPSite), it’s the equivalent programmatic element for working with site collections.

What I eventually ended up creating (thanks to some good advice from Harley Green) was a simple wrapper object which encapsulates most of the key properties of the SPSite object thus allowing basic reporting and decision making processing without the need to worry about disposing of the object.  Consider the following code snippet:

$webapp = [Microsoft.SharePoint.Administration.SPWebApplication]::Lookup("http://portal")
foreach ($site in $webapp.Sites) {
    Write-Host $site.Url

The above code results in a memory leak.  If you don’t re-loop through each SPSite object in the collection and dispose of the object by calling the “Dispose()” method you will end up with unmanaged resources left in memory that could eventually cause issues if you do a lot of processing like this (eventually the GC will dispose of the objects but that could take quite some time).

Another option would be to use a different approach to get all the SPSite objects within a web application, an approach that can be used for more dynamic querying of objects and returns back an object that would not require disposal – an SPSiteInfo object.  Here’s an example of how you could do something similar to the above using my Get-SPSite cmdlet:

foreach ($site in get-spsite -url http://portal*) {
    Write-Host $site.Url

The one obvious downside of this approach is that the wildcard means that I have to inspect every single site collection within the farm to figure out where there are matches so if you’re looking for performance this definitely isn’t the best approach.  Typically though, we’re more concerned about flexibility and ease of use rather than performance when performing the simple administrative tasks that we’d be looking to perform using PowerShell.

What I like about the approach I put together is that I can now do filtered queries without having to worry about whether or not I disposed of the objects.  Here’s an example of how to find all the site collections within the farm where the storage size is greater than 80% of the quota:

get-spsite -url * | where -filterscript {$_.Usage.Storage -ge $_.Quota.StoragemaximumLevel*.8 -and $_.Quota.StorageMaximumLevel -ne 0} | select Url,@{Name="Storage";expression={$_.Usage.Storage/1MB}}

In the above I can simply call my Get-SPSite cmdlet, filter out all items where the current storage is less than 0.8 of the maximum level if set (StorageMaximumLevel is not 0), and then display the URL and the current size, in megabytes, of the the remaining site collections.

It’s important to remember that the SPSiteInfo object is meant to be read-only as most of the properties are just copies of the variables but there are some exceptions such as the SPRecycleBinItemCollection object returned by the RecycleBin property or the SPFeatureCollection object returned by the Features property.  In general, if you have to call the Update() method of the SPSite object to save your changes then you have to use the actual SPSite object, otherwise you can work directly with the SPSiteInfo object and forego the need to instantiate and dispose of the SPSite object.

Okay, so working with properties is pretty easy and we can do some nice reports using them and even access the web application using the WebApplication property or the webs using the AllWebs collection property (all without having to dispose any of the returned objects – the AllWebs property returns a collection of SPWebInfo objects) but what about when you do need to access the actual SPSite object?  There are two approaches for this: the first is to use the SPBase property which will create a new SPSite instance and store that instance as a private member variable for future access to the property thus avoiding the overhead of creating another instance on subsequent calls; the second is to use the GetSPObject() method which creates a new instance of the SPSite object but does not store a copy so it’s a nice easy way to get an entirely new instance of the actual SPSite object (useful for when you’ve made a change which requires a reload due to caching).  In both cases you are responsible for disposing of the returned object.

The following code snippet shows the SPSiteInfo class:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Management.Automation;
   5: using System.Text;
   6: using Microsoft.SharePoint;
   7: using Microsoft.SharePoint.Administration;
   8: using Microsoft.SharePoint.Workflow;
  10: namespace Lapointe.SharePoint.PowerShell.Commands.Proxies
  11: {
  12:     public class SPSiteInfo : ISPInfo
  13:     {
  14:         private List<SPWebInfo> m_AllWebs;
  15:         private SPSite m_Site;
  17:         internal SPSiteInfo(SPSite site)
  18:         {
  19:             ID = site.ID;
  20:             AllowRssFeeds = site.AllowRssFeeds;
  21:             AllowUnsafeUpdates = site.AllowUnsafeUpdates;
  22:             ApplicationRightsMask = site.ApplicationRightsMask;
  23:             Audit = site.Audit;
  24:             CatchAccessDeniedException = site.CatchAccessDeniedException;
  25:             CertificationDate = site.CertificationDate;
  26:             ContentDatabase = site.ContentDatabase;
  27:             CurrentChangeToken = site.CurrentChangeToken;
  28:             DeadWebNotificationCount = site.DeadWebNotificationCount;
  29:             ExternalBinaryIds = site.ExternalBinaryIds;
  30:             Features = site.Features;
  31:             HostHeaderIsSiteName = site.HostHeaderIsSiteName;
  32:             HostName = site.HostName;
  33:             IISAllowsAnonymous = site.IISAllowsAnonymous;
  34:             Impersonating = site.Impersonating;
  35:             IsPaired = site.IsPaired;
  36:             LastContentModifiedDate = site.LastContentModifiedDate;
  37:             LastSecurityModifiedDate = site.LastSecurityModifiedDate;
  38:             LockIssue = site.LockIssue;
  39:             Owner = site.Owner;
  40:             Port = site.Port;
  41:             PortalName = site.PortalName;
  42:             PortalUrl = site.PortalUrl;
  43:             Protocol = site.Protocol;
  44:             Quota = site.Quota;
  45:             ReadLocked = site.ReadLocked;
  46:             ReadOnly = site.ReadOnly;
  47:             RecycleBin = site.RecycleBin;
  48:             try
  49:             {
  50:                 RootWeb = new SPWebInfo(site.RootWeb);
  51:             }
  52:             catch (Exception) {}
  53:             SearchServiceInstance = site.SearchServiceInstance;
  54:             SecondaryContact = site.SecondaryContact;
  55:             ServerRelativeUrl = site.ServerRelativeUrl;
  56:             SyndicationEnabled = site.SyndicationEnabled;
  57:             SystemAccount = site.SystemAccount;
  58:             UpgradeRedirectUri = site.UpgradeRedirectUri;
  59:             Url = site.Url;
  60:             Usage = site.Usage;
  61:             try
  62:             {
  63:                 UserAccountDirectoryPath = site.UserAccountDirectoryPath;
  64:             }
  65:             catch (UnauthorizedAccessException) { }
  66:             UserToken = site.UserToken;
  67:             WarningNotificationSent = site.WarningNotificationSent;
  68:             WebApplication = site.WebApplication;
  69:             //WorkflowManager = site.WorkflowManager;
  70:             WriteLocked = site.WriteLocked;
  71:             Zone = site.Zone;
  73:         }
  75:         /// <summary>
  76:         /// Returns a newly created instance of the object on the first access.  Subsequent accesses will utilize an internal member variable.
  77:         /// The caller is responsible for disposing of the returned object.
  78:         /// </summary>
  79:         /// <value>The SP base.</value>
  80:         public IDisposable SPBase
  81:         {
  82:             get
  83:             {
  84:                 if (m_Site == null)
  85:                     m_Site = new SPSite(ID);
  87:                 return m_Site;
  88:             }
  89:         }
  91:         /// <summary>
  92:         /// Returns a newly created instance of the object every time without storing an internal member variable for subsequent access.
  93:         /// The caller is responsible for disposing of the returned object.
  94:         /// </summary>
  95:         /// <returns></returns>
  96:         public IDisposable GetSPObject()
  97:         {
  98:             return new SPSite(ID);
  99:         }
 101:         public bool AllowRssFeeds { get; internal set; }
 102:         public bool AllowUnsafeUpdates { get; internal set; }
 103:         public List<SPWebInfo> AllWebs
 104:         {
 105:             get
 106:             {
 107:                 if (m_AllWebs != null)
 108:                     return m_AllWebs;
 110:                 m_AllWebs = new List<SPWebInfo>();
 111:                 using (SPSite site = new SPSite(ID))
 112:                 {
 113:                     foreach (SPWeb web in site.AllWebs)
 114:                     {
 115:                         try
 116:                         {
 117:                             m_AllWebs.Add(new SPWebInfo(web));
 118:                         }
 119:                         finally
 120:                         {
 121:                             web.Dispose();
 122:                         }
 123:                     }
 124:                 }
 125:                 return m_AllWebs;
 126:             }
 127:         }
 128:         public SPBasePermissions ApplicationRightsMask { get; internal set; }
 129:         public SPAudit Audit { get; internal set; }
 130:         public bool CatchAccessDeniedException { get; internal set; }
 131:         public DateTime CertificationDate { get; internal set; }
 132:         public SPContentDatabase ContentDatabase { get; internal set; }
 133:         public SPChangeToken CurrentChangeToken { get; internal set; }
 134:         public short DeadWebNotificationCount { get; internal set; }
 135:         public SPExternalBinaryIdCollection ExternalBinaryIds { get; internal set; }
 136:         public SPFeatureCollection Features { get; internal set; }
 137:         public bool HostHeaderIsSiteName { get; internal set; }
 138:         public string HostName { get; internal set; }
 139:         public Guid ID { get; internal set; }
 140:         public bool IISAllowsAnonymous { get; internal set; }
 141:         public bool Impersonating { get; internal set; }
 142:         public bool IsPaired { get; internal set; }
 143:         public DateTime LastContentModifiedDate { get; internal set; }
 144:         public DateTime LastSecurityModifiedDate { get; internal set; }
 145:         public string LockIssue { get; internal set; }
 146:         public SPUser Owner { get; internal set; }
 147:         public int Port { get; internal set; }
 148:         public string PortalName { get; internal set; }
 149:         public string PortalUrl { get; internal set; }
 150:         public string Protocol { get; internal set; }
 151:         public SPQuota Quota { get; internal set; }
 152:         public bool ReadLocked { get; internal set; }
 153:         public bool ReadOnly { get; internal set; }
 154:         public SPRecycleBinItemCollection RecycleBin { get; internal set; }
 155:         public SPWebInfo RootWeb { get; internal set; }
 156:         public SPServiceInstance SearchServiceInstance { get; internal set; }
 157:         public SPUser SecondaryContact { get; internal set; }
 158:         public string ServerRelativeUrl { get; internal set; }
 159:         public bool SyndicationEnabled { get; internal set; }
 160:         public SPUser SystemAccount { get; internal set; }
 161:         public Uri UpgradeRedirectUri { get; internal set; }
 162:         public string Url { get; internal set; }
 163:         public SPSite.UsageInfo Usage { get; internal set; }
 164:         public string UserAccountDirectoryPath { get; internal set; }
 165:         public SPUserToken UserToken { get; internal set; }
 166:         public bool WarningNotificationSent { get; internal set; }
 167:         public SPWebApplication WebApplication { get; internal set; }
 168:         //public SPWorkflowManager WorkflowManager { get; internal set; }
 169:         public bool WriteLocked { get; internal set; }
 170:         public SPUrlZone Zone { get; internal set; }
 172:     }
 173: }

The following is the code of the core Get-SPSite cmdlet:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Management.Automation;
   4: using Lapointe.SharePoint.PowerShell.Commands.OperationHelpers;
   5: using Lapointe.SharePoint.PowerShell.Commands.Validators;
   6: using Microsoft.SharePoint;
   7: using Microsoft.SharePoint.Administration;
   8: using Lapointe.SharePoint.PowerShell.Commands.Proxies;
  10: namespace Lapointe.SharePoint.PowerShell.Commands.SiteCollections
  11: {
  12:     [Cmdlet(VerbsCommon.Get, "SPSite", SupportsShouldProcess=true, DefaultParameterSetName = "Url")]
  13:     public class GetSPSiteCommand : PSCmdletBase
  14:     {
  15:         /// <summary>
  16:         /// Gets or sets the URL.
  17:         /// </summary>
  18:         /// <value>The URL.</value>
  19:         [Parameter(
  20:             ParameterSetName = "Url",
  21:             Mandatory = true,
  22:             Position = 0,
  23:             ValueFromPipeline = true,
  24:             ValueFromPipelineByPropertyName = true,
  25:             HelpMessage = "The URL of the site to return.  Supports wildcards.")]
  26:         [ValidateNotNullOrEmpty]
  27:         [ValidateUrl(true)]
  28:         public string[] Url { get; set; }
  31:         /// <summary>
  32:         /// Processes the record.
  33:         /// </summary>
  34:         protected override void ProcessRecordEx()
  35:         {
  36:             foreach (string url in Url)
  37:             {
  38:                 if (!WildcardPattern.ContainsWildcardCharacters(url))
  39:                 {
  40:                     string siteUrl = url.TrimEnd('/');
  41:                     using (SPSite site = new SPSite(siteUrl))
  42:                     {
  43:                         WriteObject(new SPSiteInfo(site));
  44:                     }
  45:                 }
  46:                 else
  47:                 {
  48:                     WildcardPattern wildCard = new WildcardPattern(url, WildcardOptions.IgnoreCase);
  49:                     if (SPFarm.Local == null)
  50:                         throw new SPException("The SPFarm object is null.  Make sure you are running as a Farm Administrator.");
  52:                     foreach (SPService svc in SPFarm.Local.Services)
  53:                     {
  54:                         if (!(svc is SPWebService))
  55:                             continue;
  57:                         foreach (SPWebApplication webApp in ((SPWebService)svc).WebApplications)
  58:                         {
  59:                             for (int i = 0; i < webApp.Sites.Count; i++)
  60:                             {
  61:                                 using (SPSite site = webApp.Sites[i])
  62:                                 {
  63:                                     if (wildCard.IsMatch(site.Url))
  64:                                         WriteObject(new SPSiteInfo(site));
  65:                                 }
  66:                             }
  67:                         }
  68:                     }
  69:                 }
  70:             }
  71:         }
  72:     }
  73: }

The following is the full help for the cmdlet.

PS C:\> get-help get-spsite -full


    Gets one or more SPSiteInfo objects representing a SharePoint 2007 Site Collection.

    Get-SPSite [-Url] <String[]> [-WhatIf] [-Confirm] [<CommonParameters>]

    Pass in a comma separated list of URLs or a string array of URLs to obtain a collection of SPSiteInfo objects.  The
    se objects do not need to be disposed.

    The SPSiteInfo object that is returned contains almost all of the same properties of the SPSite object but does not
     require disposal and should be generally considered read-only.  You can get to the actual SPSite object by using t
    he SPBase property or the GetSPObject() method.  The SPBase property results in a copy of the SPSite object being p
    ersisted in the SPSiteInfo object for faster access on future calls.  Always remember to dispose of the SPSite obje
    ct if used.  Some collection properties may be directly updated without the need to access the SPSite object.

    Copyright 2008 Gary Lapointe
      > For more information on these PowerShell cmdlets:
      > Use of these cmdlets is at your own risk.
      > Gary Lapointe assumes no liability.

    -Url <String[]>
        Specifies the URL of the site collection(s) to retrieve. Wildcards are permitted. If you specify multiple URLs,
         use commas to separate the URLs.

        Required?                    true
        Position?                    1
        Default value
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Accept wildcard characters?  false


        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false


        Required?                    false
        Position?                    named
        Default value
        Accept pipeline input?       false
        Accept wildcard characters?  false

        This cmdlet supports the common parameters: -Verbose, -Debug,
        -ErrorAction, -ErrorVariable, and -OutVariable. For more information,
        type, "get-help about_commonparameters".


    Collection of SPSiteInfo objects.


        For more information, type "Get-Help Get-SPSite -detailed". For technical information, type "Get-Help Get-SP
        Site -full".

    --------------  Example 1 --------------

    C:\PS>get-spsite -url http://portal

    This example returns back a single SPSiteInfo object.

    --------------  EXAMPLE 2 --------------

    C:\PS>$sites = get-spsite -url http://mysites/*

    This example returns back all My Site site collections under the http://mysites web application.


Note that if you receive an exception during the execution of this cmdlet simply pass in the “-debug” parameter in order to display the full stack trace which you can use to either debug yourself or report back to here to help me improve the code.

And finally – if you’ve used this cmdlet (or any others that I’ve provided) to do something cool please post your code here as a comment so that others may benefit and possibly give back some feedback that you yourself could use.