Announcing My Custom SharePoint Online Cmdlets

Posted on Posted in PowerShell Cmdlets, SharePoint Online

For quite a while now I’ve been pointing out the lack of cmdlets that are available for working with Office 365 and SharePoint 2013 (SharePoint Online) and I’ve mentioned several times that someone should really do something about that and that I’d love to be that person if only I had the time. Well, as it turns out, over Christmas break I managed to find some free time so I went ahead and got started on my own custom cmdlets for SharePoint Online which I’m now officially releasing with this post.

If you refer back to my earlier post where I detailed the SharePoint Online cmdlets provided by Microsoft, you’ll note that there are currently only 30 cmdlets – with my new offering I just about double that number by adding an additional 27 cmdlets. I’m only going to briefly outline by design goals and a couple simple examples in this article so I encourage you to visit the downloads page of my site to download the installer and/or source code for the project and for details about each cmdlet you should go to the command index page (click the SharePoint Online tab to see the cmdlets).

In creating the cmdlets I had two core goals that I wanted to achieve: first I wanted to add some basic retrieval functionality for common objects such as SPWeb and SPList objects so that I could do some simple discovery type operations just as I would with SharePoint on-premises cmdlets; second, I wanted to make working with the cmdlets and the objects returned by them much more like working with the on-premises API so that you don’t have to think about calling the Load and ExecuteQuery methods every time you try to do something. To accomplish these goals I had to not only create the actual cmdlets themselves (which was the easy part) but I also had create custom wrapper objects for several of the objects in the Microsoft.SharePoint.Client namespace. For example, when you call my Get-SPOWeb cmdlet what you get back is an SPOWeb object, not a Microsoft.SharePoint.Client.Web object. By taking this approach I an avoid the common uninitialized property errors that you’d see when you output the object to the PowerShell console. These errors makes working with the native objects nearly impossible as it requires you to know which properties are initialized and which ones aren’t – by creating a wrapper object I can hide this issue and make working with the objects very simple. I can also do things like create an Update method which handles the calling of the ExecuteQuery method as well as the refresh of the object, again, taking a lot of the nuances of working with the .NET CSOM API out of the picture. For those of you not familiar with working with the .NET CSOM API (and for those that are) the point to take away from all my technical babbling is that using my cmdlets will make working with SharePoint Online very similar to working with SharePoint on-premises.

Let’s take a look at how we can use my cmdlets. First off you need to download and install the cmdlets – I’ve created the cmdlets so that they work just like the Microsoft provided SharePoint Online cmdlets in that I just created a PowerShell V3 module manifest which references a binary assembly (dll) that contains all the cmdlets. All the installer does is put all the required files (dependent Microsoft.SharePoint.Client dlls, my assembly containing the cmdlets themselves, and the PowerShell help and format file) into an appropriate directory and then adds the path to that directory to the PSModulePath environment variable. This makes cmdlets available without having to manually load the module and is just like what the Microsoft SharePoint Online cmdlets do. Note that Microsoft provides a shortcut for the SharePoint Online Management Shell which is simply a PowerShell console with the module explicitly loaded but use of the management shell is completely unnecessary so I don’t bother creating an equivalent shortcut. To see that the module is installed correctly simply open any PowerShell console and run the Get-Module cmdlet passing in the -ListAvailable parameter. You should see the module listed as shown below:

Get-Module -ListAvailable

With the cmdlets installed you can now connect to a Site Collection using the Connect-SPOSite cmdlet. Note that the majority of my cmdlets are scoped to a single Site Collection and you must establish a connection to that Site Collection before you can do anything else; if you need to work on a different Site Collection then you’ll need to call Connect-SPOSite again.  In this example I’m connecting to a site by providing my username and the Site Collection to connect to:

Connect-SPOSite -Credential “gary@contoso.onmicrosoft.com” -Url “https://contoso.sharepoint.com”

I recommend you actually create a variable to store you credential information ($cred = Get-Credential) so that you can reuse the information each time you connect to a different Site Collection. Oh, and as a bonus, though the default behavior is to connect to SharePoint Online, if you pass in the –NetworkCredentials switch parameter then you can use these cmdlets against SharePoint on-premises – you’re welcome. To kill a connection use the Disconnect-SPOSite cmdlet (I particularly recommend you do this when creating scripts, not much point if you’re just doing something one-off in the console but if you writing a script you want to make sure you kill your context so someone can’t come behind you and hijack your credentials – this is absolutely the same pattern that the Microsoft SharePoint Online cmdlets follow so if you’re familiar with them this should not be new to you).

Once you’ve established a connection you’re ready to start using the other cmdlets. If you’d like to retrieve the root Site (SPWeb) or a child Site you can use the Get-SPOWeb cmdlet as shown:

$rootWeb = Get-SPOWebIdentity “/” -Detail

$childWeb = Get-SPOWebIdentity “/test” -Detail

Note the –Detail parameter which is useful when you want to see as much information as possible. If you’re just looking for a list Sites then I would omit the parameter so that you’re bringing back less information which should help it to run a little faster (to see all the Sites omit the –Identity parameter).

When you get back an object I encourage you to use the Get-Member cmdlet (aliased as gm) to see the list of properties and methods available. All of my cmdlets will return my own SPO* wrapper object you can always get back to the original Microsoft.SharePoint.Client object via a property on my object. The fact is I couldn’t wrap every single sub-object or every single property and method so there will be times when you’ll have to use the native object but hopefully what I did provide will at least cover a 80% of the common use cases (I hope). The following screenshot shows the members for the SPOWeb object:

SNAGHTML6491027

Notice the highlighted Web property which you can use to get to the original Microsoft.SharePoint.Client.Web object. If you need to update any properties on the object you can either use the Set-SPOWeb cmdlet or update the property directly on the object and then call the Update() method – no need to call ExecuteQuery and then refresh the object, it’s all handled for you.

I don’t want to spend time going through every cmdlet and object that I’ve created as I detail each of the cmdlets on my command index page and the object members are easily discovered using the Get-Member cmdlet so to wrap up I want to simply point out that this is definitely a work in progress and will hopefully grow over time. Also, I don’t claim to be a .NET CSOM expert so there may be some areas that can be improved upon from a performance point of view so if you download the code and see something that can be improved please share you feedback, or if you see anything else that can be improved again, please share (note that I’m absolutely horrible when it comes to responding to comments on my blog but I do eventually get to them all, just not necessarily very quickly).

Have fun and happy PowerShelling!

-Gary

34 thoughts on “Announcing My Custom SharePoint Online Cmdlets

  1. Sounds really good! I was presenting on this topic a few times and I’ve got another session scheduled in 2 weeks. Will definitely look at your solution before that and most likely integrate it into my demos.

  2. Fantastic Gary!
    One of the things that are really missing in SPO, can you do Items as well? That would enable us to do migrations to from SPO a lot easier usng scripting only.
    Thanks for a great contribution to us all!
    Regards
    // Thomas

    1. Just added 5 new cmdlets: New-SPOFile, New-SPOListItem, New-SPOListFolder, Get-SPOFile, and Get-SPOFolder. I also added several new methods to the custom SPO objects to make working with items easier.

  3. I just found your new posting and this looks like it is going to be tremendously helpful. While experimenting, i learned that Get-SPOWeb works fine, but Get-SPOweb “/” -detail and Get-SPOWeb “/childsite” need a longer path, in my case “/teams/unitest” and “teams/unites/anyteam” were required.
    Do you have any idea why the Search Center URL properties and not available? would it be possible for you to add these? Thanks for creating these commands.
    Dean

    1. Yup, for the URL you need to provide the server relative URL of the site that you’re trying to retrieve (it’s not site collection relative).

      You can see the search center URL properties by checking the AllProperties property of the SPOWeb object. Look for SRCH_SB_SET_WEB and SRCH_ENH_FTR_URL_WEB. You can add these properties or edit them if already there.

      #Get the web
      $w = Get-SPOWeb “/” -Detail
      #See all properties
      $w.AllProperties.FieldValues
      #See just the one property
      $w.AllProperties[“SRCH_ENH_FTR_URL_WEB”]
      #Change the property
      $w.AllProperties[“SRCH_ENH_FTR_URL_WEB”] = “/search/pages”
      $w.AllProperties.Update()

      Note that you’ll have to get latest for the update to work as I had a minor bug which was preventing the update from completing.

  4. Could you please add the following commands

    NewSPOSiteColumn
    NewSPOContentType
    SetSPOListContentType – Adding an existing Content Type to a list.

    I have already created the code for these as basic PowerShell functions. I would would prefer to incorporate them into something much more reusable and configurable like your commands. If you are interested in adding these or see a use for them I can provide the code I have as a starting point. Let me know how to best get the code to you. Thanks.

    1. I have a New-SPOContentType cmdlet which will allow you to add a new content type to a site or list and I also have a new New-SPOField cmdlet that adds a new field to a site or list (using AddFieldAsXml for both). I haven’t released them yet as their just a work in progress and I need to add more capabilities (like the ability to add a site column to a list rather than just create a new list scoped column). If you want to send me your code I’d be happy to take a look at it, just know that I do have some in progress already so depending on you approach I may or may not be able to leverage it.

      1. I have created very basic PowerShell functions that iterate over an XML document to produce the SharePoint entities, Site Column, Content Type etc.

        The XML is the variable you see being passed into each of these functions.

        This code may or may not be useful but you are happy to have it.

        Does your New-SPOContentType command allow the option to add existing Site Columns to the Content Type? or is there an option to call a Add-SPOField command to add the Site Column to the Content Type after the Content Type has been created.
        Will your New-SPOContentType command allow existing Site Content Types to be added to a list?

        My scenario is I have created an XML document that has hundreds of Site Columns to provision. I then describe lots of Content Types in the XML and the Site Columns that belong to each Content Type and should be added to the Content Type. I then create lots of lists and libraries (also described in the XML) and then add the Content Types to the lists.

        The code below does most of that but not all. Your scripts are also a lot nicer to work with and not as complicated as I am sure I am making this.

        Is there some where I can learn about creating cmdlets in PowerShell?

        I understand the main part of each cmdlet because it is all familiar SharePoint stuff but creating the cmdlets and all the non SharePoint PowerShell stuff is a bit confusing. Like PipeBind and class decorators etc.

        Thanks.

        function process_SiteColumns ($siteColumns) {
        [Microsoft.SharePoint.Client.FieldCollection] $webSiteColumns = $ctx.Web.AvailableFields

        foreach ($siteColumn in $siteColumns) {
        info(“Adding Site Column {” + $siteColumn.Name + “}…”)

        try {
        # Check if the field already exist
        $existingSiteColumn = $webSiteColumns.GetByInternalNameOrTitle($siteColumn.Name)
        $ctx.Load($existingSiteColumn)
        $ctx.ExecuteQuery()
        } catch {
        error (“ERROR: {” + $_.Exception.Message + “}”)
        }

        # The title value for the field will be null if the field does not exist but the object will not be null
        if ($existingSiteColumn.Title -eq $null) {
        try {
        [Microsoft.SharePoint.Client.Field] $newSiteColumn = $ctx.Web.Fields.AddFieldAsXml($siteColumn.OuterXml, $false, [Microsoft.SharePoint.Client.AddFieldOptions]::DefaultValue)
        $newSiteColumn.Title = $siteColumn.DisplayName
        $newSiteColumn.Update()
        $ctx.Load($newSiteColumn)
        $ctx.ExecuteQuery()
        info (“Site Column {” + $newSiteColumn + “} added.”)
        } catch {
        error (“ERROR: {” + $_.Exception.Message + “}”)
        }
        } else {
        warning (“Site Column {” + $siteColumn.Name + “} already exists.”)
        }
        }
        }

        function process_ContentTypes ($contentTypes) {
        [Microsoft.SharePoint.Client.FieldCollection] $webSiteColumns = $ctx.Web.AvailableFields
        [Microsoft.SharePoint.Client.ContentTypeCollection] $webContentTypes = $ctx.Web.AvailableContentTypes

        foreach ($contentType in $contentTypes) {
        info (“Adding Content Type {” + $contentType.Name + “}…”)
        [Microsoft.SharePoint.Client.ContentTypeCreationInformation] $contentTypeCreation = New-Object Microsoft.SharePoint.Client.ContentTypeCreationInformation
        $contentTypeCreation.Name = $contentType.Name
        $contentTypeCreation.Description = $contentType.Description
        $contentTypeCreation.Group = $contentType.Group
        $contentTypeCreation.ParentContentType = $webContentTypes[$contentType.ParentName]

        try {
        # Check if the content type already exists
        $newContentType = $ctx.Web.ContentTypes.Add($contentTypeCreation)
        $ctx.Load($newContentType)
        $ctx.ExecuteQuery()
        info (“Content Type {” + $newContentType.Name + “} added.”)
        } catch {
        error (“ERROR: {” + $_.Exception.Message + “}”)
        }

        [Microsoft.SharePoint.Client.ContentTypeCollection] $existingWebContentTypes = $ctx.Web.AvailableContentTypes
        $ctx.Load($existingWebContentTypes)
        $ctx.ExecuteQuery()

        foreach ($siteColumn in $contentType.SiteColumns.ChildNodes) {
        info (“Adding Site Column {” + $siteColumnToAdd.Title + “} to Content Type {” + $contentType.Name + “}…”)
        [Microsoft.SharePoint.Client.FieldLinkCreationInformation] $fieldLinkCreation = New-Object Microsoft.SharePoint.Client.FieldLinkCreationInformation

        try {
        # Check if the field already exists
        $siteColumnToAdd = $webSiteColumns.GetByInternalNameOrTitle($siteColumn.InternalName)
        $ctx.Load($siteColumnToAdd)
        $ctx.ExecuteQuery()
        } catch {
        error (“ERROR: {” + $_.Exception.Message + “}”)
        }

        if ($siteColumn.DisplayName -ne $null) {
        $siteColumnToAdd.Title = $siteColumn.DisplayName
        }

        $fieldLinkCreation.Field = $siteColumnToAdd

        foreach ($existingContentType in $existingWebContentTypes) {
        [Microsoft.SharePoint.Client.ContentType] $foundContentType = $existingContentType

        if ($foundContentType.Name -eq $contentType.Name) {
        try {
        $foundContentType.FieldLinks.Add($fieldLinkCreation)
        $foundContentType.Update($true)
        $ctx.Load($foundContentType)
        $ctx.ExecuteQuery()
        info (“Site Column {” + $siteColumnToAdd.Title + “} added to Content Type {” + $contentType.Name + “}”)
        break
        } catch {
        error (“ERROR: {” + $_.Exception.Message + “}”)
        }
        }
        }
        }
        }
        }

        function process_Lists ($lists) {
        foreach ($list in $lists) {
        # Check if List already exists
        try {
        [Microsoft.SharePoint.Client.List] $existingList = $ctx.Web.Lists.GetByTitle($list.Title)
        $ctx.Load($existingList)
        $ctx.ExecuteQuery()
        } catch {
        error (“ERROR: {” + $_.Exception.Message + “}”)
        }

        if ($existingList -eq $null) {
        try {
        info(“Adding List {” + $list.Title + “}…”)
        [Microsoft.SharePoint.Client.ListCreationInformation] $listCreation = New-Object Microsoft.SharePoint.Client.ListCreationInformation
        $listCreation.Title = $list.ListWebAddress
        $listCreation.Description = $list.Description
        $listCreation.TemplateType = [int] $list.TemplateType
        [Microsoft.SharePoint.Client.List] $newList = $ctx.Web.Lists.Add($listCreation)
        $newList.Title = $list.Title
        $newList.Update()
        $ctx.ExecuteQuery()
        info(“List {” + $newList.Title + “} added.”)
        } catch {
        error (“ERROR: {” + $_.Exception.Message + “}”)
        }
        } else {
        warning (“List {” + $list.Title + “} already exists.”)
        }
        }
        }

        function process_ContentTypesToLists ($spEntities) {
        foreach ($spEntity in $spEntities) {
        [Microsoft.SharePoint.Client.ContentTypeCollection] $existingWebContentTypes = $ctx.Web.AvailableContentTypes
        [Microsoft.SharePoint.Client.List] $existingList = $ctx.Web.Lists.GetByTitle($spEntity.ListTitle)

        $ctx.Load($existingWebContentTypes)
        $ctx.Load($existingList)

        $ctx.ExecuteQuery()
        $contentTypeExists = $false

        # Check if Content Type already exists on the List
        foreach ($existingContentType in $existingWebContentTypes) {
        if ($existingContentType.Name -eq $spEntity.ContentTypeName) {
        $contentTypeExists = $true
        break
        }
        }

        if ($spEntity.Action -eq “Add”) {
        if ($contentTypeExists -eq $false) {
        if ($existingList.ContentTypesEnabled -eq $false) {
        $existingList.ContentTypesEnabled = $true
        }

        foreach ($existingContentType in $existingWebContentTypes) {
        if ($existingContentType.Name -eq $spEntity.ContentTypeName) {
        try {
        info(“Adding Content Type {” + $spEntity.ContentTypeName + “} to List {” + $spEntity.ListTitle + “}…”)
        $newContentType = $existingList.ContentTypes.AddExistingContentType($existingContentType)
        $existingList.Update()
        $ctx.ExecuteQuery()
        info(“Content Type {” + $spEntity.ContentTypeName + “} added to List {” + $spEntity.ListTitle + “}”)
        break
        } catch {
        error (“ERROR: {” + $_.Exception.Message + “}”)
        }
        }
        }
        } else {
        warning (“Content Type {” + $spEntity.ContentTypeName + “} already exists on List {” + $spEntity.ListTitle + “}”)
        }
        } elseif ($spEntity.Action -eq “Delete”) {
        foreach ($existingContentType in $ctx.Web.AvailableContentTypes) {
        if ($existingContentType.Name -eq $spEntity.ContentTypeName) {
        try {
        info(“Deleting Content Type {” + $spEntity.ContentTypeName + “} from List {” + $spEntity.ListTitle + “}…”)
        $existingList.ContentTypes[$spEntity.ContentTypeName].DeleteObject()
        [Microsoft.SharePoint.Client.ContentTypeCollection] $existingListContentTypes = $existingList.ContentTypes
        $ctx.Load($existingListContentTypes)
        $existingList.Update()
        $ctx.ExecuteQuery()
        info(“Content Type {” + $spEntity.ContentTypeName + “} deleted from List {” + $spEntity.ListTitle + “}”)
        break
        } catch {
        error (“ERROR: {” + $_.Exception.Message + “}”)
        }
        }
        }
        }
        }
        }

      2. I am currently using my own implementation of the SiteColumn and ContentType cmdlets but would be interested when you plan to release yours.

        When using the Remove-SPOList, I get a confirmation to delete the list. I understand this is because you have ConfirmImpact = ConfirmImpact.High on the cmdlet but adding the -Confirm $false to the command still prompts to delete the list. Do you know how I stop it prompting?

        Do you plan on adding the ability to create publishing pages and add web parts to pages?

        Thanks.

  5. After using the Get-SPOListItem how do I go about updating some of the metadata values?
    Is there a property I can access to update a field?
    Something similar to the New-SPOFile cmdlet with the -FieldValues attribute would be good to have on a new cmdlet called Set-SPOListItem -FieldValues

    Thanks.

  6. Hi Gary,
    I have tried to download this and when I use the installed nothing appears to run but the .msi file is removed. Does this need a 64-bit machine?

    I have windows 7 SP1 running on a 32-bit machine and any help would be appreciated… Thanks.

  7. This looks amazing, can’t wait to use these in my upcoming SP Online project.

    One question however – we are planning to work from Windows 8.1 and use your cmdlets in our PowerShell F5 deploy script.

    Do you have any suggestions for how to include them without being able to deploy the WSP? Maybe extract the DLLs and load the modules dynamically somehow?

    1. The SPO cmdlets are just a module – no WSP. All the installer is doing is making it so that the cmdlets are available without having to explicitly run Import-Module. If you install and copy the DLL to your build machine you should be able to integrate them pretty easily.

  8. Thanks for your efforts, Gary. However I must be doing something wrong!!! I downloaded and installed the SPOcmdlets but when I try and use one I get term not recognized. I ran the get-modules command and see your assembly (v0.0). Any ideas?

    1. Not really enough info for me to troubleshoot. One thing to watch out for is to make sure you open a new powershell window after the install. You can also try to explicitly load the module (shouldn’t be necessary but worth trying).

  9. Hi Gary,
    As per the Instructions, I have installed SPO commands and i am able to get custom commands in Powershell Window but when i run Get-SPOWeb, i am getting following error
    Get-SPOWeb : Method not found: ‘System.String Microsoft.SharePoint.Client.Web.get_AlternateCssUrl()’.
    At line:1 char:1
    + Get-SPOWeb
    + ~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Get-SPOWeb], MissingMethodException
    + FullyQualifiedErrorId : System.MissingMethodException,Lapointe.SharePointOnline.PowerShell.Cmdlets.Sites.GetSPOWeb

    Kindly let me know any suggestion to resolve this error.

    Thanks

  10. Hey Gary,

    You are a life saver! I was fretting over SharePoint online knowing it had a watered down version of the PowerShell I was used to.

    I’ve ran the msi Installer and opened a new PowerShell window as Administrator but when I do a
    Get-Module -ListAvailable I don’t see your modules loaded? I’m running Windows 8 64 bit.
    How do I manually import your cmdlets using import-module?

    1. It should automatically be available, not sure why it isn’t. However, you can manually load it:
      Import-Module “C:\Program Files (x86)\Falchion Consulting, LLC\SharePoint Online PowerShell\Lapointe.SharePointOnline.PowerShell\Lapointe.SharePointOnline.PowerShell.psd1”

      1. I’m having the same problem but your suggestion isn’t working for me. Running as admin. I’ve verified the path. C:\Program Files (x86)\Falchion Consulting, LLC\SharePoint Online PowerShell\Lapointe.SharePointOnline.PowerShell but there’s no Lapointe.SharePointOnline.PowerShell.psd1 file, as far as I can tell

        1. Not really sure what’s up. I uploaded another copy of the .msi just in case the original was somehow screwed up (that .psd1 file should be there). Regardless, you can use the Import-Module cmdlet to load the .dll file directly if the .psd1 isn’t there.

          1. Hi Gary – whatever you did worked! I now see it with the -listavailable command after running your installer (never showed up before) Thanks so much for the reply!

  11. Hey there Gary – quick question about running your these cmdlets… whenever I run New-SPOWeb I receive the error “New-SPOWeb : File Not Found.” Any thoughts on whats going on?

    I have my script set up like so:

    $UserCredential = Import-Clixml c:\users\usernam\documents\cred.xml
    $projnum = Read-host “Please enter the project number”
    $ProjectName = Read-host “Please enter the project name”
    $InternalSiteUrl = “https://walshgroup.sharepoint.com/sites/$projnum/”

    Connect-SPOSite -Url $InternalSiteUrl -Credential $UserCredential

    New-SPOWeb -ParentWeb “$InternalSiteUrl” -Url “external” -Webtemplate “{5917F12E-8176-41B7-B7DE-958869E893C3}#External_Template_v06” -Title “$ProjectName” -UniquePermissions $False

    Disconnect-SPOSite

    1. Not sure. I do see that you’re trying to pass in $False to a switch parameter – if you don’t want to use unique permissions then just omit the parameter (to explicitly set it to false use: -UniquePermissions:$false). You can try to get the full error details which might tell you more about what’s going on: $Error[0] | select *

      If I had to guess though I’d speculate that it’s having trouble locating the web template you’ve specified (maybe use fiddler to see what the actual CSOM call is returning to see if it sheds some light).

Leave a Reply

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

CAPTCHA

*