Search code examples
powershellpowershell-3.0powershell-4.0powershell-5.0

Powershell New-WebBinding Pipeline Issue with Name Property


I'm encountering an issue with New-WebBinding when piping in an object. I have an object that defines 5 properties: Name, Protocol, Port, IPAddress and HostHeader (all 5 are supported in the New-WebBinding cmdlet as Accept Pipeline input: ValueByPropertyName). However, when you pipe in this object, it still requests a Name: to be submitted. Here is a quick test function if you'd like to duplicate the issue. If you hit enter at the prompt, it successfully processes the objects, adding the bindings. But the prompt itself breaks it as a non-interactive script.

I've tested this with both PS v3 and PS v4.

I'm pretty sure I'm doing this all correctly but wanted to make sure there wasn't something I might be overlooking. For now I'm just iterating through my object collection in a foreach loop which does not have this issue but would like to see if this is a bug I should report.

function Test-WebBinding{
   [CmdletBinding()]
   Param()

   $testBindingCol = @()

   $testBinding1 = New-Object System.Object
   $testBinding1 | Add-Member -MemberType NoteProperty -Name Name -Value 'Default Web Site'
   $testBinding1 | Add-Member -MemberType NoteProperty -Name Protocol -Value 'https'
   $testBinding1 | Add-Member -MemberType NoteProperty -Name Port -Value '4000'
   $testBinding1 | Add-Member -MemberType NoteProperty -Name IPAddress -Value '*'
   $testBinding1 | Add-Member -MemberType NoteProperty -Name HostHeader -Value 'Test4000'
   $testBindingCol += $testBinding1

   $testBinding2 = New-Object System.Object
   $testBinding2 | Add-Member -MemberType NoteProperty -Name Name -Value 'Default Web Site'
   $testBinding2 | Add-Member -MemberType NoteProperty -Name Protocol -Value 'http'
   $testBinding2 | Add-Member -MemberType NoteProperty -Name Port -Value '4001'
   $testBinding2 | Add-Member -MemberType NoteProperty -Name IPAddress -Value '*'
   $testBinding2 | Add-Member -MemberType NoteProperty -Name HostHeader -Value 'Test4001'
   $testBindingCol += $testBinding2

   $testBindingCol | New-WebBinding
}

Solution

  • PetSerAl is onto the right idea with his comment above:

    One workaround would be to change current location to some site (cd IIS:\Sites\SomeSite), it does not really matter to which

    That does actually work, but why doesn't it work from the normal file system prompt?

    To discover why New-WebBinding behaves this way I loaded the Microsoft.IIS.PowerShell.Provider assembly containing this and other WebAdministration cmdlets into dotPeek. The assembly lives in the GAC so you tell dotPeek to "Open from GAC".

    When loaded, the class we're interested in is called NewWebBindingCommand.

    Upon a cursory inspection we can see that all of the parameter properties are decorated with the [Parameter(ValueFromPipelineByPropertyName = true)] attribute so that's a good start, piping an array of objects with matching property names ought to work:

    enter image description here

    NewWebBindingCommand ultimately inherits from System.Management.Automation.Cmdlet and in this instance is overriding the BeginProcessing method. If overridden, BeginProcessing is called by PowerShell and "Provides a one-time, preprocessing functionality for the cmdlet."

    It is important to understand that a cmdlet's BeginProcessing override is called before any pipeline fed named parameters are processed and bound to the cmdlet's properties (see: Cmdlet Processing Lifecycle (MSDN)) .

    Our New-WebBinding cmdlet's implementation of BeginProcessing looks like:

    protected override void BeginProcessing()
    {
      base.BeginProcessing();
      if (!string.IsNullOrEmpty(this.siteName))
        return;
      this.siteName = this.GetSiteName("Name");
    }
    

    this.siteName is the private member value for the Name property which would be bound to the -Name parameter. When we arrive at the if(...) statement above `this.siteName isn't yet bound (it's null) and so falls through to:

    this.siteName = this.GetSiteName("Name");
    

    The call to GetSiteName() calls up to the cmdlet's immediate base class HelperCommand which provides a number of "helper" methods that are useful to many different WebAdministration cmdlets.

    HelperCommand.GetSiteName(string prompt) looks like this:

    protected string GetSiteName(string prompt)
    {
      PathInfo pathInfo = this.SessionState.PSVariable.Get("PWD").Value as PathInfo;
      if (pathInfo != null && pathInfo.Provider.Name.Equals("WebAdministration", StringComparison.OrdinalIgnoreCase))
      {
        string[] strArray = pathInfo.Path.Split('\\');
        if (strArray.Length == 3 && strArray[1].Equals("sites", StringComparison.OrdinalIgnoreCase))
          return strArray[2];
      }
      if (!string.IsNullOrEmpty(prompt))
        return this.PromptForParameter<string>(prompt);
      return (string) null;
    }
    

    For the purpose of learning about this issue I created my own PowerShell cmdlet (called the Kevulator, sorry) and pulled in the New-WebBinding cmdlet's BeginProcessing() code and the code from New-WebBinding's base class helper method GetSiteName().

    Here's a screenshot of breaking inside GetSiteName in VS2015 when attached to a PowerShell session piping in your bindings to New-Kevulator:

    enter image description here

    The top arrow demonstrates that our Name property backed by siteName hasn't yet been bound and is still null (which as a mentioned above causes GetSiteName to be executed). We've also just stepped past a break point on this line:

    PathInfo pathInfo = 
            this.SessionState.PSVariable.Get("PWD").Value as PathInfo;
    

    ...which determines what kind of path provider we're sitting at. In this case we're on a normal filesystem C:\> prompt so the provider name will be FileSystem. I've highlighted this with the second arrow.

    If we Import-Module WebAdministration and CD IIS: then re-run and break again you can see that the path provider has changed to WebAdministration which is responsible for handling life inside IIS:> and beyond:

    enter image description here

    If the pathInfo name is not equal to WebAdministration, i.e. we're not at an IIS: prompt, then the code falls through and will prompt for the Name parameter at the command line, just as you've experienced.

    If the pathInfo value is WebAdministration then one of two things will happen:

    If the path is IIS: or IIS:\Sites the code falls through and issues a prompt for the Name parameter.

    If the path is IIS:\Sites\SomeSiteName then SomeSiteName is returned and effectively becomes the -Name parameter value and we bail out from the GetSiteName() function back to BeginProcessing.

    That last behaviour is useful because if your current path is say IIS:\Sites\MySite then you can pipe in an array of bindings for that site but without specifying the Name parameter. Alternatively if you do provide a Name property in your piped array of objects then these will override the default site name picked up from your IIS:\Sites\MySite context.

    So, getting back to our code, once BeginProcessing has run its course our pipeline named parameters are now actually bound and then the ProcessRecord method is called, which is the meat and potatoes work that New-WebBinding has to perform.

    TLDR:

    New-WebBinding won't bind pipelined parameters unless you change your current working directory to an IIS website, e.g, cd iis:\Sites\MySite.