Simple way of extending PowerShell objects with custom methods

As you may know, IIS configuration is extensible. You could add new section, extend existing one by adding new schema file into folder containing schemas. All this was described in full details in the article of Tobin Titus. When we provide methods on configuration elements with COM extension, the only way to call it using configuration APIs is through creation of method instance, then creation of input parameters element on this instance, etc -- quite convoluted way. In PowerShell we expose it in more convenient way, like intrinsic methods on the object. User is able to call these methods exactly the same way as any other intrinsic methods of given object. Let's see how runtime methods work on site element:

>$site = get-webconfiguration '/system.applicationHost/sites/site[@name="mysite"]'

>$site.Stop()

>$site.State

Stopped

>$site.Start()

>$site.State

Started

Methods that are coming from configuration extension could be called directly. Properties from extension are updated dynamically, like property State on web site object. In this post I will explain how it is done.

PowerShell SDK provides number of ways to add new functionality to any object. You could add note property, script property or code property. You also could add script and code methods. When I started my implementation, my first attempt was to expose extended methods as PSCodeMethod, but this requires code reference to real code that will be executed when my exposed method is called. That was a problem. This real code has to be generated dynamically. It is simple enough code that will take input parameters and do all required actions to call configuration API. It could be implemented using type DynamicMethod, but it has to be done through emitting code into buffer, and this is quite tricky even for simple code. After spending couple of days with code generators and managed assembly language reference I started looking for alternative.

PowerShell PSCodeMethod inherits from PSMethodInfo type. PSMethodInfo is an abstract class that PowerShell uses to work with extended methods: it has name, description and Invoke method. You could inherit your type from PSMethodInfo and it will be used exactly the same way as regular code method. Problem is that property Name is not virtual and there is no way to set it from inherited class. Fortunately, you could do it through reflection. After I realized that, coding took just couple of hours. PSMethodInfo is public class, therefore it has stay the same in future, and property Name shouldn't go away. Using reflection here is OK -- it is a workaround for mistake in type design.

You will find implementation code at the end of this post. Two methods of CodeMethod class are specific to IIS 7 configuration: one is Invoke() -- that uses configuration APIs to call extension method, and another one is GetDefinition() that produces human readable signature for the methods using method definition from IIS configuration schema. You could use the same approach for any other type of method: defined in XML, or in WMI, or in ADSI, any time when you have programmatically accessible method definition and some generic way to invoke this method, for example passing its name and parameters. With direct implementation of extension method you don't need to bend your code to be compatible with PSCodeMethod -- you could access your native method directly.

Class CodeProperty is implemented very similarly, the only difference is that it inherits from PSPropertyInfo and declares itself as type PSPropertyInfo.CodeProperty. You also have to do the same dirty hack with name. In CodeProperty methods get() and set() call native implementation, because of this property value is refreshing at every access. Let's see how this will looks like in PowerShell.

$s = get-webconfiguration "//sites/site[@name='test']"

$s | gm Start

 TypeName: Microsoft.IIs.PowerShell.Framework.ConfigurationElement

Name  MemberType Definition
----  ---------- ----------
Start CodeMethod void Start()

$s.bindings.Collection[0] | gm AddSslCertificate | fl *

TypeName : Microsoft.IIs.PowerShell.Framework.ConfigurationElement
Name : AddSslCertificate
MemberType : CodeMethod
Definition : void AddSslCertificate(System.String certificateHash, System.String certificateStoreName)

Method AddSslCertificate() is an extension, defined on each binding element of the site, and it has the following schema:

<element name="bindings">
  <collection addElement="binding" clearElement="clear">
  ...
  <method name="AddSslCertificate" extension="Microsoft.ApplicationHost.RscaExtension">
     <inputElement>
        <attribute name="certificateHash" type="string" />
        <attribute name="certificateStoreName" type="string" defaultValue="MY" />
     </inputElement>
  </method>
   ... 

How to use CodeMethod? Every time you have to return your object into PowerShell, you will convert it into PSObject, right? If you not doing this, PowerShell will do it for you, and it don't know that you have extended methods that has to be exposed in some specific way. So, you have to add those methods manually.

PSObject myObject = new PSObject()

myObject.Methods.Add(new CodeMethod("methodName", configurationMethod));

...send object to PowerShell

In my case I am adding CodeMethod created from extended configuration method instance and name of the method. In your case you could pass and store any other type that will be used to call native method implementation. 

 

Enjoy,

Sergei Antonov

Here is CodeMethod implementation (provided as is without any support):

public class CodeMethod : PSMethodInfo
{
   private ConfigurationMethod configMethod;
   string definition;
   public CodeMethod(string name, ConfigurationMethod method)
   {
      FieldInfo nameField = this.GetType().GetField("name", 
          BindingFlags.NonPublic | BindingFlags.Instance);
      if (nameField != null)
      {
          nameField.SetValue(this, name);
      }
      configMethod = method;
      definition = GetDefinition(method);
   }
   public override PSMemberTypes MemberType
   {
      get {return PSMemberTypes.CodeMethod;}
   }
   public override Collection<string> OverloadDefinitions
   {
      get
      {
          Collection<string> returnValue = new Collection<string>();
          returnValue.Add(this.definition);
          return returnValue;
      }
   }
   public override string ToString()
   {
      StringBuilder returnValue = new StringBuilder();
      foreach (string overload in OverloadDefinitions)
      {
         returnValue.Append(overload);
         returnValue.Append(", ");
      }
      returnValue.Remove(returnValue.Length - 2, 2);
      return returnValue.ToString();
   }
   public override string TypeNameOfValue
   {
      get { return typeof(CodeMethod).FullName;}
   }
    public override PSMemberInfo Copy()
   {
      return new CodeMethod(Name, configMethod);
   }
   // This code is specific to IIS 7 configuration, 
   // and I put it here for illustration
   public override object Invoke(params object[] arguments)
   {
      if (arguments == null)
      {
          throw new ArgumentNullException("arguments");
      }
      ConfigurationMethodInstance mi = configMethod.CreateInstance();
      ConfigurationElementSchema input 
         = configMethod.Schema.InputSchema;
      if (arguments.Length > 0)
      {
          if (input != null)
          {
              int parametersCount = 0;
              if (input.AttributeSchemas != null)
              {
                  parametersCount += input.AttributeSchemas.Count;
              }
              if (input.ChildElementSchemas != null)
              {
                  parametersCount += input.ChildElementSchemas.Count;
              }
              if (arguments.Length != parametersCount)
              {
                  throw new ArgumentException();
              }
              PSObject argObject = new PSObject();
              // We produced definition from attributes first, then 
              // from child elements. 
              // We expect the same order on input.
              int index = 0;
              if (input.AttributeSchemas != null)
              {
                  foreach (ConfigurationAttributeSchema s 
                     in input.AttributeSchemas)
                  {
                     argObject.Properties.Add(
                        new PSNoteProperty(s.Name, 
                        arguments[index++]));
                  }
              }
              if (input.ChildElementSchemas != null)
              {
                  foreach (ConfigurationElementSchema s 
                     in input.ChildElementSchemas)
                  {
                     argObject.Properties.Add(
                        new PSNoteProperty(s.Name, 
                        arguments[index++]));
                  }
              }
              mi.Input.Update(argObject);
          }
      }
      try
      {
          mi.Execute();
      }
      catch (COMException ex)
      {
          if (ex.ErrorCode == unchecked((int)0x80070490))
          {
             // This happens when there is no binding 
             // for FTP on the site.
          }
          else
          {
             throw;
          }
      }
      if (mi.Output != null)
      {
          if (configMethod.Schema
                 .OutputSchema.AttributeSchemas.Count == 1)
          {
              return mi.Output.Attributes[0].ToPSObject();
          }
          else
          {
              return mi.Output.ToPSObject(null);
          }
      }
      return null;
  }
  private static string GetDefinition(ConfigurationMethod method)
  {
      ConfigurationMethodSchema ms = method.Schema;
      ConfigurationElementSchema input = ms.InputSchema;
      ConfigurationElementSchema output = ms.OutputSchema;
       StringBuilder builder = new StringBuilder();
      if (output != null)
      {
          if (output.AttributeSchemas.Count == 1)
          {
              // Configuration returns any result as element. 
              // We could produce more reasonable signature here
              ConfigurationAttributeSchema a 
                 = output.AttributeSchemas[0];
              builder.Append(a.ClrType.Name);
          }
          else
          {
              builder.Append("object");
          }
      }
      else
      {
          builder.Append("void");
      }
      builder.Append(" ");
      builder.Append(ms.Name);
      builder.Append("(");
      if (input != null)
      {
          foreach (ConfigurationAttributeSchema a 
             in input.AttributeSchemas)
          {
              builder.Append(a.ClrType);
              builder.Append(" ");
              builder.Append(a.Name);
              builder.Append(", ");
          }
           if (input.ChildElementSchemas != null)
          {
              foreach (ConfigurationElementSchema child 
                 in input.ChildElementSchemas)
              {
                  builder.Append("object");
                  builder.Append(" ");
                  builder.Append(child.Name);
                  builder.Append(", ");
              }
          }
          builder.Remove(builder.Length - 2, 2);
       }
       builder.Append(")");
       return builder.ToString();
    }
}

No Comments