How we use XPath in IIS PowerShell namespace

IIS 7 configuration API's were designed as DOM: you always have to start from top level data structure and work your way to the data you need, either collection element or child element attributes. It is quite annoying model when you are building generic tools, and soon we started looking for some way of cutting off amount of code required for access of internal collections and attributes. At some moment we realized that we could use System.Xml.XPath.XPathNavigator type to expose IIS configuration. This type could be used to represent any hierarchical data as tree for XPath queries, not necessarily originated in XML. That was a perfect fit: with XPath query we could access any entity inside of configuration in one step. First CTP of provider was based on this approach. It worked nice but was pretty slow. Keeping XPath engine in managed code introduced major performance bottleneck: configuration data are in non-managed code, and XPath engine should marshal across the border every piece of data during processing of query. To make it better, fearless IIS developer William Moy created XPath query processor in native code, following the same model as XPathNavigator, and second CTP of provider is based on this engine. It is so effective, that when we access internal data inside of configuration section using fully qualified path it takes in average only 10% longer than using straight code based on explicit configuration API's, when this code accesses the same data. Fully qualified here means that path points to the unique data inside of configuration, and query don't need to walk around big parts of tree. As you probably know, configuration system in IIS 7 is distributed, and is merged from multiple configuration files located in root folders of virtual directories and file system folders below them. Path that points to any of these containers for configuration files named commit path. XPath engine works with the search tree built from IIS configuration sections, merged from files which were gathered along some commit path. The tree has root, represented by root section group, then it includes child groups and sections, then child elements and collection elements, so it is kind of like Object Viewer tree in Visual Studio, rather than file system tree. If written properly, XPath query could return just one unique piece of data from configuration, in that case we could use it as universal addressing format for configuration data. Or it could select set of nodes from the search tree, in that case it is good to describe groups of data, for example, all sites, or all applications of the site, etc. If we store these queries into some place, we could describe our own namespace, composed from data extracted from configuration by these stored queries. XPath queries are the backbone of IIS 7 PowerShell namespace: if you open file NavigationTypes.xml, you will see that most of things in the namespace are described as XPath queries, for example, 

<virtualDirectory>
  ...
  <InstanceXPath>%parent::InstanceXPath%/virtualDirectory[@path="%path%"]</InstanceXPath>
  ...
  <children>
    <childInfo>
      <childType>fileDirectory</childType>
      <xPath>%InstanceXPath%/physicalPath</xPath>
      ...
    </childInfo>
    <childInfo>
      <childType>file</childType>
      <xPath>%InstanceXPath%/physicalPath</xPath>
      ...
    </childInfo>
  </children>
</virtualDirectory>

 

This description instructs code in provider how to list virtualDirectory namespace nodes, how to produce list of child nodes for this node. Names in %% are macros referring to namespace object properties, or its parent properties. When user wants to get object, provider resolves InstanceXPath using specific instance as parent and sends expanded query strings to XPath engine, then it uses resulting selection to build namespace object. This approach is very flexible: if I want to rearrange the tree, all I need to do is to modify this XML file; if I want to add new namespace type, based on configuration object from some section, in most cases I only add its description to this XML tree. After restart of PowerShell IIS provider will resolve it and new type will be exposed in the namespace. Format of namespace description was changed a bit after CTP2 to enable addition of new description files, so you could use this model to extend our namespace and include your objects.

XPath queries directly accessible for user through base level commands: get-webconfiguration and set-webconfiguration. There are two more commands that work with properties, they also consume XPath queries to address object owning properties. Our XPath engine actually works with two trees, one combined from sections, elements and attributes, and another one -- from commit paths. This commit path engine is used, for example,  when we want to retrieve configuration recursively starting from some entry point on commit path tree, say, from some particular site. When you run command

get-webconfiguration /system.webServer/asp/limits/@requestQueueMax "iis:\sites\default web site" -recurse

it will return all setting for asp section attribute requestQueueMax, existing down the tree from "Default Web Site". To direct query down the tree, provider internally resolves namespace path, and when -recurse is specified, converts it into "MACHINE/WEBROOT/APPHOST/Default Web Site/descendant-or-self::node()". This expression tells query engine that it should not limit search by one level, and should go down the tree. Another part, configuration XPath is also modified to "/system.webServer/asp/limits/@requestQueueMax[is-locally-defined()]". Function in square brackets is specific to IIS configuration navigator, it returns "true" if attribute was defined on some particular level, otherwise query will return all inherited values for this attribute. Our XPath engine defines several functions like this one, I will describe all of them later.

This is an example of simplest XPath query. You could write much more complicated expressions and get more specific data from configuration. I recommend to learn a bit about XPath, if you want to use our "platform" level functionality to write you own scripts and functions in PowerShell, you will be surprised, how much you could achieve in just couple of line. You could find pretty good practical courses in XPath in many places in the Net, some sites offer visual XPath editor that shows result of your query applied to sample data file.

Almost all helper commands that we released in CTP2 were built very quickly and are all based on base level commands. Each simplified cmdlet is nothing more but piece of code that declares parameters, then compiles low level command from user input and calls get-webconfiguration or other command. You could do the same for cmdlet specific to your needs. If you are using PowerShell v2.0, you could write script based cmdlets instead of compiled code. Let me show you one example of helper command.

PS IIS:\#> get-command new-webbinding | select definition | fl *

Definition : New-WebBinding [-Site] <String> [-Port <UInt32>] [-IPAddress <String>] [-HostHeader <String>] [-Ssl] [-Force] ...

 

For clarity I removed parameters inserted by PowerShell. This cmdlet defines all parameters that we need to create binding on some site: site name, port, ipaddress, host header and ssl switch, that tells us that we have to build binding for ssl. Internal code then compiles another command:

 

new-itemproperty IIS:\sites\<site_name> -name Bindings -value @{protocol="http";bindingInformation="<IPAddress>:<Port>:<Host_Header>"}

 

Here is the source code for ProcessRecord method of the cmdlet, it uses couple of utility functions to format error message and to invoke resulting script. I assume that you are familiar with details of PowerShell SDK and know how to write cmdlets. PowerShell v1.0 doesn't show errors happened in internally called script, therefore I have to check $error variable and if there is an error, print it in my utility function. In v2.0 that was already fixed.

 ...

 protected override void ProcessRecord()
{
     StringBuilder cmdLine = new StringBuilder("new-itemproperty ");
     string protocol = "http";
     if (sslBinding.IsPresent)
     {
         protocol = "https";
     }
     cmdLine.Append("\"IIS:\\sites\\" + siteName + "\" ");
     cmdLine.Append(" -name Bindings -value @{protocol=\"");
     cmdLine.Append(protocol);
     cmdLine.Append("\";bindingInformation=\"");
     if (!ipAddress.Equals("*"))
     {
         IPAddress test = IPAddress.Any;
         if (!IPAddress.TryParse(ipAddress, out test))
         {
             throw new ArgumentException(

                 Utility.FormatResourceString(

                    "InvalidIPAddress", ipAddress));
         }
     }
     cmdLine.Append(ipAddress);
     if (sitePort == 0xFFFFFFFF)
     {
         sitePort = (uint)(sslBinding.IsPresent ? 443 : 80);
     }
     cmdLine.Append(":" + sitePort.ToString() + ":");
     if (!String.IsNullOrEmpty(hostHeader))
     {
         cmdLine.Append(hostHeader);
     }
     else if (!sslBinding.IsPresent)
     {
         cmdLine.Append(siteName);
     }
     cmdLine.Append("\"}");
     if (Force.IsPresent)
     {
         cmdLine.Append(" -force");
     }
     Utility.InvokeScriptShowError(this,

         cmdLine.ToString());
}

 ...

That's all you need to make it work. It is a simple command, most of source code is required to declare and describe parameters, I spent half an hour to write it, and as a result user could avoid typing expression for PowerShell hastable @{...}, so unpopular in PowerShell MVP circles and could omit some parameters altogether.

In my next post I will show you some less trivial queries that could produce more exciting results.

 

Sergei Antonov

 

No Comments