SharePoint Automation Gary Lapointe – Founding Partner, Aptillon, Inc.

19Oct/095

Creating Custom SharePoint 2010 Cmdlets using Visual Studio 2010

With SharePoint 2010 we now have the ability to create custom PowerShell cmdlets that can be deployed just like any other SharePoint artifact using SharePoint Solution Packages (WSP) created with Visual Studio 2010. With SharePoint 2007 it was necessary to build a custom setup (MSI) package which had to be run on every server in the farm. This setup package would register a custom snap-in that you'd have to create which would be responsible for registering all of your custom cmdlets with the PowerShell runtime.

With SharePoint 2010 we no longer have to create a custom snap-in or setup package. When the Microsoft.SharePoint.PowerShell snap-in is loaded it examines the {SharePointRoot}/Config/PowerShell/Registration folder for any XML files and dynamically registers the cmdlets specified in the XML. As long as the SharePoint binaries have been installed on the server then you can utilize this feature (if the farm has not yet been created then you'll have to manually GAC the assembly and deploy the registration XML file as solution deployments only work when the farm exists).

To facilitate a standard and consistent scripting experience SharePoint 2010 introduces five new base classes that all SharePoint 2010 PowerShell cmdlets should be derived from:

SharePoint 2010 PowerShell Cmdlet Base Classes

When creating your custom cmdlet you should carefully choose the correct base class for your cmdlet. When creating a cmdlet that is meant to work with persistent objects (objects that are to be used across calls) you should utilize one of the four task based base classes: SPRemoveCmdletBase, SPNewCmdletBase, SPSetCmdletBase, or SPGetCmdletBase. When creating cmdlets that return non-persistent objects/data or perform tasks that do not require a persistent object (e.g., Start-SP*) then you should use the SPCmdlet base class. A good example of a cmdlet that would use the SPCmdlet base class would be one what returns a report or some other information without returning back any specific objects.

Let's now take a look at an example of a custom cmdlet that we'll eventually package up in a SharePoint Solution Package:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.PowerShell;
using Microsoft.SharePoint.Administration;
using System.Management.Automation;

namespace Lapointe.SharePoint2010.PowerShell.Demo.Quotas
{
[Cmdlet(VerbsCommon.Get, "SPQuotaTemplate"),
SPCmdlet(RequireLocalFarmExist = true, RequireUserFarmAdmin = true)]
public class SPCmdletGetQuotaTemplate : SPGetCmdletBase<SPQuotaTemplate>
{
protected override void InternalValidate()
{
if (this.Identity != null)
{
base.DataObject = this.Identity.Read();
if (base.DataObject == null)
{
base.WriteError(new PSArgumentException("The quota template does not exist."), ErrorCategory.InvalidArgument, this.Identity);
base.SkipProcessCurrentRecord();
}
}
}

protected override IEnumerable<SPQuotaTemplate> RetrieveDataObjects()
{
List<SPQuotaTemplate> list = new List<SPQuotaTemplate>();
if (base.DataObject != null)
{
list.Add(base.DataObject);
return list;
}
SPWebService webService = SPWebService.ContentService;
if (webService != null)
{
foreach (SPQuotaTemplate quota in webService.QuotaTemplates)
{
list.Add(quota);
}
}

return list;
}

[Parameter(Mandatory = false, ValueFromPipeline = true, Position = 0), Alias(new string[] { "Name" })]
public SPQuotaTemplatePipeBind Identity
{
get;
set;
}
}
}

In the code example above I'm returning back SPQuotaTemplate objects based on the Identity (or Name) passed into the cmdlet. If the Identity parameter is not provided then all quota templates are returned to the pipeline. In the InternalValidate method I'm checking if the Identity parameter has been provided, and if it has, I set the base class's DataObject property by calling the Read method of the SPQuotaTemplatePipeBind object. In the override RetrieveDataObjects method I then check the DataObject property and return the value as an item in a generic list. If the DataObject property has not been set then I loop through all existing quota templates and return them as generic list. Note that if you are returning lots of items or large items it is better, and preferable, to directly call the WriteResult method and return back null - for this case I know there are typically not a lot of templates and they are not large so I just return back a single collection rather than calling WriteResult.

Pay particular attention to the SPQuotaTemplatePipeBind type - In SharePoint an object can be represented in numerous ways, for example, an SPSite object can be represented by either an URL or a GUID. In order to prevent the need to multiple parameters to support these various types Microsoft has introduced the PipeBind object which eliminates the need for these superfluous parameters and from having to create multiple parameter sets to support them. In the case of the SPQuotaTemplatePipeBind object I can pass in either an actual instance of an SPQuotaTemplate object or a name representing a quota template.

You're not limited to what is available out of the box. You can easily create your own PipeBind objects by simply inheriting from the SPCmdletPipeBind class. Take a look at the following example which demonstrates how to create a custom SPListPipeBind object:

using System;
using System.Collections.Generic;
using Microsoft.SharePoint;
using Microsoft.SharePoint.PowerShell;
using System.Management.Automation;
using System.Globalization;

namespace Lapointe.SharePoint2010.PowerShell.Demo.Lists
{
public sealed class SPListPipeBind : SPCmdletPipeBind<SPList>
{
private bool m_IsAbsoluteUrl;
private bool m_IsCollection;
private Guid m_SiteGuid;
private Guid m_WebGuid;
private Guid m_ListGuid;
private string m_WebUrl;
private string m_ListUrl;

public SPListPipeBind(SPList instance)
: base(instance)
{
}

public SPListPipeBind(Guid guid)
{
this.m_ListGuid = guid;
}

public SPListPipeBind(string inputString)
{
if (inputString != null)
{
inputString = inputString.Trim();
try
{
this.m_ListGuid = new Guid(inputString);
}
catch (FormatException)
{
}
catch (OverflowException)
{
}
if (this.m_ListGuid.Equals(Guid.Empty))
{
this.m_ListUrl = inputString;
if (this.m_ListUrl.StartsWith("http", true, CultureInfo.CurrentCulture))
{
this.m_IsAbsoluteUrl = true;
}
if (WildcardPattern.ContainsWildcardCharacters(this.m_ListUrl))
{
this.m_IsCollection = true;
}
}
}
}

public SPListPipeBind(Uri listUri)
{
this.m_ListUrl = listUri.ToString();
}

protected override void Discover(SPList instance)
{
this.m_ListGuid = instance.ID;
this.m_WebGuid = instance.ParentWeb.ID;
this.m_SiteGuid = instance.ParentWeb.Site.ID;
}

public override SPList Read()
{
return this.Read(null);
}

public SPList Read(SPWeb web)
{
SPList list = null;
string parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url : {0}", new object[] { "Empty or Null" });
if (this.IsCollection)
{
return null;
}
try
{
if (Guid.Empty != this.ListGuid)
{
if (web == null && Guid.Empty != this.m_WebGuid && Guid.Empty != this.m_SiteGuid)
{
parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url: {0} and Web Id: {1}", new object[] { this.ListGuid.ToString(), this.m_WebGuid.ToString() });
using (SPSite site = new SPSite(this.m_SiteGuid))
{
web = site.OpenWeb(this.m_WebGuid);
list = web.Lists[ListGuid];
}
}
else
{
parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url: {0} and web Url {1}", new object[] { this.ListUrl, web.Url });
list = web.Lists[ListGuid];
}
}
else if (!string.IsNullOrEmpty(this.ListUrl))
{
string serverRelativeListUrl = null;
if (this.m_IsAbsoluteUrl)
{
serverRelativeListUrl = Utilities.GetServerRelUrlFromFullUrl(this.ListUrl).Trim('/');
}
else
{
serverRelativeListUrl = this.ListUrl.Trim('/');
}
if (web == null)
{
parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url : {0}", new object[] { this.ListUrl });
using (SPSite site = new SPSite(this.ListUrl))
{
web = site.OpenWeb();
}
}
else
{
parameterDetails = string.Format(CultureInfo.CurrentCulture, "Id or Url : {0} and web Url {1}", new object[] { this.ListUrl, web.Url });
}
if (!web.Exists)
{
list = null;
}
else
{
list = web.GetList(serverRelativeListUrl);
}
}
}
catch (Exception exception)
{
throw new SPCmdletPipeBindException(string.Format("The SPList Pipebind object could not be found ({0}).", parameterDetails), exception);
}
if (list == null)
{
throw new SPCmdletPipeBindException(string.Format("The SPList Pipebind object could not be found ({0}).", parameterDetails));
}
return list;
}

public bool IsCollection
{
get
{
return this.m_IsCollection;
}
}

public Guid ListGuid
{
get
{
return this.m_ListGuid;
}
}

public string ListUrl
{
get
{
return this.m_ListUrl;
}
}
}
}

There are two core components that are required for a custom PipeBind object. The first is to have a constructor that takes in the type that you wish to convert (in this example, a string, URI, or GUID) to the target object. The second is to override the Read method which is used to convert the argument value passed into the constructor into the target type. In some cases you'll need additional information which must be provided by the calling code - for example, if a GUID is passed in, representing the List ID, then you will also need to provide the SPWeb object which contains the List; this is done by creating an overload for the Read method which accepts an SPWeb object. It's up to the calling code to determine which overload to call.

Let's now look at how we can package our SPCmdletGetQuotaTemplate class into a SharePoint Solution Package using Visual Studio 2010.

From a new instance of Visual Studio 2010:

  1. Click File > New > Project to create a new Visual Studio Project
  2. In the New Project dialog select Visual C#/SharePoint/2010 in the Installed Templates panel and then select Empty Project:

    New Project Dialog

  3. After you click OK you will be taken to the SharePoint Configuration Wizard:

    SharePoint Configuration Wizard

    You can specify any site to use for debugging as we won't be using it for PowerShell development (note that when you start the debugger you'll be given a warning if the specified site's web.config does not allow debugging). PowerShell cmdlets must be deployed to the GAC so select Deploy as full-trust solution and click the Finish button to create the project.

The first thing we need to do with our new empty project is to add a couple of project references:

  1. Right-click the References folder in the project and select Add Reference...
  2. In the Add Reference dialog's .NET tab select Microsoft.SharePoint.PowerShell and System.Management.Automation
  3. Click OK to add the references to the project

Now that we have our references added we can setup our project structure. PowerShell cmdlets are not deployed using Features so we can delete the starting Feature folder that is created:

  1. Expand the Features folder
  2. Right-click the Feature1 Feature and click Delete

The next step is to add a SharePoint Mapped Folder:

  1. Right-click the project and click Add > SharePoint Mapped Folder...
  2. Add the {SharePointRoot}/Config/PowerShell/Registration folder
    1. Note that you can add the Format and Help folders as well but I won't be using those in this example as creating help and format files are outside the scope of this article (I usually will add the {SharePointRoot}/Config/PowerShell folder and then manually add the three sub-folders so that I can keep things grouped together in one parent folder within my project).
  3. Click OK to add the mapped folder
    1. If a folder is created under the Registration folder then go ahead and delete it (this sub-folder is automatically added in Beta1 but may not be added come RTM)

In the new Registration mapped folder create a new XML file (you can name it anything you like but I usually give it the same name as my project) and paste the following XML into the file:

<?xml version="1.0" encoding="utf-8" ?>
<ps:Config xmlns:ps="urn:Microsoft.SharePoint.PowerShell"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:Microsoft.SharePoint.PowerShell SPCmdletSchema.xsd">

<ps:Assembly Name="$SharePoint.Project.AssemblyFullName$">
<ps:Cmdlet>
<ps:VerbName>Get-SPQuotaTemplate</ps:VerbName>
<ps:ClassName>Lapointe.SharePoint2010.PowerShell.Demo.Quotas.SPCmdletGetQuotaTemplate</ps:ClassName>
<ps:HelpFile>Lapointe.SharePoint2010.PowerShell.Demo.dll-help.xml</ps:HelpFile>
</ps:Cmdlet>
</ps:Assembly>
</ps:Config>

Note that the <ps:HelpFile /> element does require a value but the file specified does not have to exist.

Now we simply need to paste in the code for the SPCmdletGetQuotaTemplate class from above:

  1. Create a folder below the project root called Quotas
  2. Add a new class file named SPCmdletGetQuotaTemplate.cs
  3. Paste the code from above into this file (be sure to adjust your namespaces in the class file and the XML file if you used a different project name than the one shown)

You now have a complete SharePoint 2010 PowerShell Solution - all that's left is to build and deploy it:

  1. Right-click the project name and select Deploy

Notice what is happening in the output window - IIS application pools are being recycled along with the retraction and deployment of the solution. Because this is a PowerShell solution we don't need IIS to be recycled so let's create a new deployment configuration to remove the recycling of the application pools which should speed up our deployment time:

  1. Right-click the project and select Properties
  2. In the properties dialog select the Deploy tab
  3. In the Edit Configurations group select New to create a new deployment action
  4. Name the new deployment action PowerShell and configure the deployment steps as shown below:Add New Deployment Configuration
  5. Click OK to save the new deployment configuration

Now that we have our custom deployment configuration we need to tell our project to use this configuration. Make sure the Properties Window is visible (type F4 if not) and select the project. Select the PowerShell configuration we just created in the Active Deployment Configuration drop-down.

Our final configuration setting change is to configure the project so that it will open PowerShell when we start the debugger:

  1. Right-click the project and select Properties to return to the project's properties dialog
  2. Click the Debug tab
  3. Select the radio button next to Start external program and specify the following value: C:\Windows\System32\WindowsPowerShell\v1.0\PowerShell.exe
  4. Paste the following into the Command line arguments text box: -NoExit  " & ' C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\CONFIG\POWERSHELL\Registration\sharepoint.ps1 ' "

You can now start the debugger (F5) which will load a PowerShell console and register the SharePoint 2010 snap-in which results in the loading of your new custom cmdlet. To verify that the cmdlet is loaded type Get-Command Get-SPQuotaTemplate | Format-List. You should see the following output:

PS C:\> Get-Command Get-SPQuotaTemplate | Format-List

Name             : Get-SPQuotaTemplate
CommandType      : Cmdlet
Definition       : Get-SPQuotaTemplate [[-Identity] <SPQuotaTemplatePipeBind>]
                   [-AssignmentCollection <SPAssignmentCollection>] [-Verbose]
                   [-Debug] [-ErrorAction <ActionPreference>] [-WarningAction <
                   ActionPreference>] [-ErrorVariable <String>] [-WarningVariab
                   le <String>] [-OutVariable <String>] [-OutBuffer <Int32>]

Path             :
AssemblyInfo     :
DLL              : C:\Windows\assembly\GAC_MSIL\Lapointe.SharePoint2010.PowerSh
                   ell.Demo\1.0.0.0__xxxxxxxxxxxxxxxx\Lapointe.SharePoint2010.P
                   owerShell.Demo.dll
HelpFile         : C:\Program Files\Common Files\Microsoft Shared\Web Server Ex
                   tensions\14\CONFIG\PowerShell\Help\Lapointe.SharePoint2010.P
                   owerShell.Demo.dll-help.xml
ParameterSets    : {[[-Identity] <SPQuotaTemplatePipeBind>] [-AssignmentCollect
                   ion <SPAssignmentCollection>] [-Verbose] [-Debug] [-ErrorAct
                   ion <ActionPreference>] [-WarningAction <ActionPreference>]
                   [-ErrorVariable <String>] [-WarningVariable <String>] [-OutV
                   ariable <String>] [-OutBuffer <Int32>]}
ImplementingType : Lapointe.SharePoint2010.PowerShell.Demo.Quotas.SPCmdletGetQu
                   otaTemplate
Verb             : Get
Noun             : SPQuotaTemplate

As you can see, creating and deploying custom PowerShell cmdlets for SharePoint 2010 using Visual Studio 2010 is now super easy. The only complexity lies in the logic of the cmdlet itself.

As you probably expected I have already been hard at work on creating some new cmdlets to replace some of my old PowerShell cmdlets as well as a few select STSADM commands. I'll be releasing these new cmdlets with full source shortly - keep checking back here for more example code and downloads!

Comments (5) Trackbacks (0)
  1. Gary–

    Very nice article! As the PM who designed the PowerShell implementation for Foundation Server I’m happy to see you outlining all the important pieces and to use the same consistancy model we have internally (Identity parameter, pipe binds, etc).

    Keep these articles coming! Looking forward to seeing your custom cmdlets…

    -Zach Rosenfield

  2. I followed the instructions above but couldn’t find Microsoft.SharePoint.PowerShell under .NET references. I am using SP2010 public beta and VS2010 professional beta. What am I missing?

  3. You have to edit the project file using notepad and manually add the references. I’ll be updating my post shortly (this was supposed to be fixed in b2 but I found out this past week that it won’t be fixed).

  4. Thanks Gary.

    I tried adding the assembly reference by browing to the DLL location in C:\Windows\Assembly\GAC_MSIL\Microsoft.SharePoint.PowerShell. I was able to add the reference this way.

  5. Hi Gary, thanks for this great post! I am just now creating a Recycle-SPContext Cmdlet, and using your tricks to get it packaged and deployed.

    Ref. http://social.msdn.microsoft.com/Forums/en-US/sharepoint2010general/thread/5ea46e07-a4b0-4bab-b698-9b6eeb695607


Leave a comment

CAPTCHA Image

*

No trackbacks yet.