PowerShell commandlets – Sitecore PowerShell Extensions pt. 2

Reason

If you’re a PowerShell novice like me, staring blankly at the official Sitecore PowerShell Extensions site thinking “I’m too lazy to bother with a new syntax, how can I keep writing C# and still make use of this tool?”, PowerShell commandlets are the way to go; commandlets can be written and run with minimal use of PowerShell syntax.

Luckily, writing new commandlets and registering these with the Sitecore PowerShell Extensions is very straight forward.

As is evident from the links above, there’s already a ton of info to be found regarding writing commandlets. Hence the following examples focus on:

At first glance, the goal of avoiding PowerShell syntax as much as possible probably makes little sense. After all, what’s the point of using a technology when you’re not willing to put in the time and effort to fully understand it?
Introducing new technology into a solution should never be done carelessly, as it can entail significant costs to the customer and/or your employer. The problem isn’t learning PowerShell as an individual developer – it’s potentially forcing every colleague who’ll work on the project to learn PowerShell as well. It’s only prudent to minimize the learning curve and hence maintenance cost for any technology not in widespread use in your company.

Examples are based on .NET 4.5, Sitecore 7.1 rev. 130926 and Sitecore PowerShell Extensions 2.5.1.

Note: This is part 2 of 3 regarding the concept of using the RemoteAutomation web service in the creation and deletion of test content as part of a integration test suite.

Code

Create and register a commandlet

The following commandlet simply allows us to create a user based on the parameters “domain”, “user name” and “password”. All three parameters are mandatory and can’t be null or empty, as indicated by the property attributes.
When the commandlet is successfully executed, the users full name (e.g. “sitecore\Uli Weltersbach”) is written as output via the call to WriteObject(...).

using System;
// The System.Management.Automation.dll can be annoying to reference.
// See http://stackoverflow.com/questions/1186270/referencing-system-management-automation-dll-in-visual-studio for further details.
using System.Management.Automation;
using Sitecore.Exceptions;
using Sitecore.Security.Accounts;
using Sitecore.SecurityModel;

[Cmdlet("Create", "User")]
public class CreateUserCommand : PSCmdlet
{
  [Parameter(Mandatory = true)]
  [ValidateNotNullOrEmpty]
  public string DomainName { get; set; }

  [Parameter(Mandatory = true)]
  [ValidateNotNullOrEmpty]
  public string UserName { get; set; }

  [Parameter(Mandatory = true)]
  [ValidateNotNullOrEmpty]
  public string Password { get; set; }

  protected override void ProcessRecord()
  {
    if (!DomainManager.DomainExists(DomainName))
      throw new UnknownDomainException(string.Format("Domain \"{0}\" doesn't exist.", DomainName));
    string fullUserName = DomainManager.GetDomain(DomainName).GetFullName(UserName);
    if (User.Exists(fullUserName))
      throw new InvalidOperationException(string.Format("User \"{0}\" already exists.", fullUserName));
    User.Create(fullUserName, Password);
    WriteObject(fullUserName);
  }
}

To make the CreateUser commandlet available in Sitecore, save the configuration shown below to a .config-file (e.g. “PowerShell.Commandlets.config”) and place it in the “App_Config/Include”-folder.
Modify namespace and assembly names as needed.

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
        <add name="CreateUserCommand"  type="NamespaceName.CreateUserCommand, AssemblyName"/>
      </commandlets>
    </powershell>
  </sitecore>
</configuration>

Once a commandlet is registered within Sitecore, it can be used via the PowerShell console. Invoking the CreateUser commandlet requires minimal knowledge of PowerShell syntax as shown below. The tab key can be used for autocompletion when typing part of a commandlet name. All logic that would have required complex PowerShell syntax to execute is tucked away in the commandlet itself:

CreateUserCommand shown in Sitecore PowerShell ISE

CreateUserCommand local execution

Generate the PowerShell script required to run a commandlet

When executing a commandlet programmatically through e.g. the RemoteAutomation web service, the amount of PowerShell syntax required drops to zero when using the following technique:

  1. Create an instance of a commandlet.

  2. Set it’s properties using strongly typed objects.

  3. Use reflection to generate the PowerShell syntax required to invoke the commandlet.

<

p>The ScriptBuilder class shown below performs the reflection based script generation. It currently supports most of the types commonly used when working with Sitecore, and collections of these. It can be extended easily to accommodate additional data types by adding overrides of the AppendFormattedValue method.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Management.Automation;
using System.Reflection;
using System.Text;
using Microsoft.CSharp.RuntimeBinder;

public class ScriptBuilder
{
  // These are some of the "automatic variables" available in PowerShell.
  // See http://technet.microsoft.com/en-us/library/hh847768.aspx for further details.
  private const string True = "$True";
  private const string False = "$False";
  private const string Null = "$Null";

  private readonly PSCmdlet _command;
  private readonly StringBuilder _script = new StringBuilder();

  public ScriptBuilder(PSCmdlet command)
  {
    if (command == null)
      throw new ArgumentNullException("command");
    _command = command;
  }

  public string CreateScript()
  {
    AppendCommandName();
    IEnumerable<PropertyInfo> properties = GetParameterProperties();
    foreach (PropertyInfo property in properties)
    {
      AppendParameterName(property);
      AppendParameterValue(property);
    }
    return _script.ToString();
  }

  private void AppendCommandName()
  {
    CmdletAttribute cmdletAttribute = _command.GetType().GetCustomAttribute<CmdletAttribute>();
    if (cmdletAttribute == null)
      throw new NullReferenceException(string.Format("Type {0} isn't decorated with {1}.", _command.GetType(), typeof(CmdletAttribute)));
    _script.AppendFormat("{0}-{1}", cmdletAttribute.VerbName, cmdletAttribute.NounName);
  }

  private IEnumerable<PropertyInfo> GetParameterProperties()
  {
    return (from property in _command.GetType().GetProperties()
            let parameterAttribute = property.GetCustomAttribute<ParameterAttribute>()
            where parameterAttribute != null
            orderby parameterAttribute.Position
            select property).ToArray();
  }

  private void AppendParameterName(PropertyInfo property)
  {
    _script.AppendFormat(" -{0}", property.Name);
  }

  private void AppendParameterValue(PropertyInfo property)
  {
    object value = property.GetValue(_command);
    if (value == null)
    {
      _script.AppendFormat(" {0}", Null);
      return;
    }
    try
    {
      AppendFormattedValue(value as dynamic);
    }
    catch (RuntimeBinderException ex)
    {
      throw new NotSupportedException(string.Format("Property {0} contains a value of type {1} which isn't supported.", property.Name, property.PropertyType), ex);
    }
  }

  private void AppendFormattedValue(string value)
  {
    _script.AppendFormat(" '{0}'", EscapeSingleQuotes(value));
  }

  private string EscapeSingleQuotes(string value)
  {
    return value.Replace("'", "`'");
  }

  private void AppendFormattedValue(int value)
  {
    _script.AppendFormat(" {0}", value.ToString(CultureInfo.InvariantCulture));
  }

  private void AppendFormattedValue(double value)
  {
    _script.AppendFormat(" {0}", value.ToString(CultureInfo.InvariantCulture));
  }

  private void AppendFormattedValue(bool value)
  {
    _script.AppendFormat(" {0}", value ? True : False);
  }

  private void AppendFormattedValue(DateTime value)
  {
    _script.AppendFormat(" '{0}'", value.ToString("O"));
  }

  private void AppendFormattedValue(Guid value)
  {
    AppendFormattedValue(value.ToString());
  }

  private void AppendFormattedValue<T>(IEnumerable<T> values)
  {
    const string valueSeparator = ",";
    foreach (T value in values)
    {
      AppendFormattedValue(value as dynamic);
      _script.Append(valueSeparator);
    }
    _script.Remove(_script.Length - valueSeparator.Length, valueSeparator.Length);
  }
}

Example

Shown below is an example of the ScriptBuilder class generating the PowerShell syntax required to invoke a commandlet:

Generate PowerShell script using the ScriptBuilder class

One thought on “PowerShell commandlets – Sitecore PowerShell Extensions pt. 2

  1. Pingback: Update to State of knowledge for PowerShell Extensions for Sitecore – November 2014 | Codality

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s