Calling a Webservice from PowerShell

One question that comes up fairly frequently in our internal mailing list, the newsgroup, and the internet at large is how to call a webservice from PowerShell. In fact, several excellent PowerShellers have written about it: Keith, and Geert. In general, the guidance has been to use wsdl.exe to generate the webservice proxy, compile that proxy into a DLL, then finally load that DLL into memory.

This is a topic that I cover in my upcoming book, and initially wrote a script to automate these proxy generation steps. However, the prerequisite to running a script designed in that matter is fairly huge. Wsdl.exe doesn't come with the .NET Framework, so you need to have the .NET SDK installed. That was something that made me uncomfortable, so I instead opted for a solution that could generate the web service proxy without wsdl.exe.

The .NET Framework supports a few classes that make this possible, although the documentation for them is terrible at best 🙂

To give a glimpse into the writing process behind my upcoming "Windows PowerShell - The Definitive Guide" (O'Reilly,) I'll occasionally post entries "as the author sees it." This entry discusses calling a webservice from PowerShell.

 

Program: Connect-WebService

Although "screen scraping" (parsing the HTML of a web page) is the most common way to obtain data from the internet, web services are becoming increasingly common. Web services provide a significant advantage over HTML parsing, as they are much less likely to break when the web designer changes minor features in their design.

The only benefit to web services isn't their more stable interface, however. When working with web services, the .NET Framework allows you to generate proxies that let you interact with the web service as easily as you would work with a regular .NET object. That is because to you, the web service user, these proxies act almost exactly the same as any other .NET object. To call a method on the web service, simply call a method on the proxy.

The primary difference you will notice when working with a web service proxy (as opposed to a regular .NET object) is the speed and internet connectivity requirements. Depending on conditions, a method call on a web service proxy could easily take several seconds to complete. If your computer (or the remote computer) experiences network difficulties, the call might even return a network error message instead of the information you had hoped for.

 The following script allows you to connect to a remote webservice, if you know the location of its service description file (WSDL.) It generates the web service proxy for you, allowing you to interact with it as you would any other .NET object.

Example 9-3. Connect-WebService.ps1

##############################################################################

## Connect-WebService.ps1

##

## Connect to a given web service, and create a type that allows you to

## interact with that web service.

##

## Example:

##

##     $wsdl = "http://terraserver.microsoft.com/TerraService2.asmx?WSDL"

##     $terraServer = Connect-WebService $wsdl

##     $place = New-Object Place

##     $place.City = "Redmond"

##     $place.State = "WA"

##     $place.Country = "USA"

##     $facts = $terraserver.GetPlaceFacts($place)

##     $facts.Center

##############################################################################

param(

    [string] $wsdlLocation = $(throw "Please specify a WSDL location"),

    [string] $namespace,

    [Switch] $requiresAuthentication)

 

## Create the web service cache, if it doesn't already exist

if(-not (Test-Path Variable:\Lee.Holmes.WebServiceCache))

{

    ${GLOBAL:Lee.Holmes.WebServiceCache} = @{}

}

 

## Check if there was an instance from a previous connection to

## this web service. If so, return that instead.

$oldInstance = ${GLOBAL:Lee.Holmes.WebServiceCache}[$wsdlLocation]

if($oldInstance)

{

    $oldInstance

    return

}

 

## Load the required Web Services DLL

[void] [Reflection.Assembly]::LoadWithPartialName("System.Web.Services")

 

## Download the WSDL for the service, and create a service description from

## it.

$wc = new-object System.Net.WebClient

 

if($requiresAuthentication)

{

    $wc.UseDefaultCredentials = $true

}

 

$wsdlStream = $wc.OpenRead($wsdlLocation)

 

## Ensure that we were able to fetch the WSDL

if(-not (Test-Path Variable:\wsdlStream))

{

    return

}

 

$serviceDescription =

    [Web.Services.Description.ServiceDescription]::Read($wsdlStream)

$wsdlStream.Close()

 

## Ensure that we were able to read the WSDL into a service description

if(-not (Test-Path Variable:\serviceDescription))

{

    return

}

 

## Import the web service into a CodeDom

$serviceNamespace = New-Object System.CodeDom.CodeNamespace

if($namespace)

{

    $serviceNamespace.Name = $namespace

}

 

$codeCompileUnit = New-Object System.CodeDom.CodeCompileUnit

$serviceDescriptionImporter =

    New-Object Web.Services.Description.ServiceDescriptionImporter

$serviceDescriptionImporter.AddServiceDescription(

    $serviceDescription, $null, $null)

[void] $codeCompileUnit.Namespaces.Add($serviceNamespace)

[void] $serviceDescriptionImporter.Import(

    $serviceNamespace, $codeCompileUnit)

 

## Generate the code from that CodeDom into a string

$generatedCode = New-Object Text.StringBuilder

$stringWriter = New-Object IO.StringWriter $generatedCode

$provider = New-Object Microsoft.CSharp.CSharpCodeProvider

$provider.GenerateCodeFromCompileUnit($codeCompileUnit, $stringWriter, $null)

 

## Compile the source code.

$references = @("System.dll", "System.Web.Services.dll", "System.Xml.dll")

$compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters

$compilerParameters.ReferencedAssemblies.AddRange($references)

$compilerParameters.GenerateInMemory = $true

 

$compilerResults =

    $provider.CompileAssemblyFromSource($compilerParameters, $generatedCode)

 

## Write any errors if generated.        

if($compilerResults.Errors.Count -gt 0)

{

    $errorLines = ""

    foreach($error in $compilerResults.Errors)

    {

        $errorLines += "`n`t" + $error.Line + ":`t" + $error.ErrorText

    }

 

    Write-Error $errorLines

    return

}

## There were no errors.  Create the webservice object and return it.

else

{

    ## Get the assembly that we just compiled

    $assembly = $compilerResults.CompiledAssembly

 

    ## Find the type that had the WebServiceBindingAttribute.

    ## There may be other "helper types" in this file, but they will

    ## not have this attribute

    $type = $assembly.GetTypes() |

        Where-Object { $_.GetCustomAttributes(

            [System.Web.Services.WebServiceBindingAttribute], $false) }

 

    if(-not $type)

    {

        Write-Error "Could not generate web service proxy."

        return

    }

 

    ## Create an instance of the type, store it in the cache,

    ## and return it to the user.

    $instance = $assembly.CreateInstance($type)

    ${GLOBAL:Lee.Holmes.WebServiceCache}[$wsdlLocation] = $instance

    $instance

}

 

[Update: Several readers have requested that this script support web services that reqire credentials, and support web services that return the same type of object. Added parameters to allow this.]

13 Responses to “Calling a Webservice from PowerShell”

  1. Tommy Williams writes:

    Lee: won’t this bit:

    if(-not (Test-Path $wsdlPath))
    {
    return
    }

    always be true since [IO.Path]::GetTempFileName() creates the file?

    Or does System.Net.WebClient.DownloadFile() delete the file if it has an error or exception?

    OK, OK, I went ahead and tried it and, indeed, if you give it a non-working path (like http://doesnotexist.foo) it throws an exception and deletes the specified file.

    That behavior surprises me.

  2. Lee writes:

    It surprises me too 🙂

    As Lutz is my witness:

    System.Net.WebClient.DownloadFile(Uri, String) : Void

    (…)

    try
    {
    stream1 = new FileStream(fileName, FileMode.Create, FileAccess.Write);
    (…)
    flag1 = true;
    }

    (…)

    finally
    {
    if (stream1 != null)
    {
    stream1.Close();
    if (!flag1)
    {
    File.Delete(fileName);
    }
    stream1 = null;
    }
    this.CompleteWebClientState();
    }

  3. Tom writes:

    Lee,

    Thanks for the great starter. I’ve used this as a great cmdlet for talking with SOAP services. One thing I’m having trouble with though. I’m trying to use the cmdlet against a SOAP server that is causing the script to die with "The server committed a protocol violation." I’ve searched google to find this, "some webservers do not follow the correct RFC specification when performing specific HTTP web requests, in order to communicate with them we need to enable unsafe header parsing."

    I’m stummped at how to do this using reflection. Here is a link I found that has what I think is the correct solution http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=296969&SiteID=1 however I’m unsure on how to port it over to PS. Any help you can provide would be great.

    Tom

  4. Lee writes:

    Hi Tom;

    Try this: http://www.leeholmes.com/blog/ConvertingCToPowerShell.aspx

  5. Tom writes:

    Lee,

    You rock. I was almost there I had the following and was getting stuck on the Invoke and bindings..

    $root = "System.Net.Configuration"
    $type = "$root.SettingsSection" -as [type] ## $type = [System.Net.Configuration.SettingsSection]
    $aNetAssembly = [System.Reflection.Assembly]::GetAssembly($type) -as [object] ## Get the assembly that contains the internal class type
    $aSettingsType = $aNetAssembly.GetType("System.Net.Configuration.SettingsSectionInternal") -as [type]

    and I was working on the InvokeMethod but not a lot of examples exist on the net. I was trying to create an invoker similar to this ..

    $aSettingsType.InvokeMember(
    "Section",
    [Reflection.BindingFlags]::InvokeMethod,
    $null,
    $null,
    $args
    )

    but was failing. You blog shows a very clean example now which makes much more sense. Thanks for taking the time to review with me.

    Tom

  6. Keith J. Farmer writes:

    Cool

    Now optionally persist the assembly (or at least the WSDL) to disk, and allow instantiation with a different url.

    After all, WSDL shouldn’t change once it’s established. 🙂

  7. Johan Akerstrom writes:

    Absolutely brilliant code! So useful. Thanx a big bunch!

  8. David Choi writes:

    Hello. Thanks for the script, but I’m having some trouble using it with sharepoint. Specifically I’m trying to call the UserGroup service, http://server/_vti_bin/UserGroup.asmx?WSDL. But I get the error "There is an error in XML document" at the line $serviceDescription = [Web.Services.Description.ServiceDescription]::Read($wsdlStream). Thanks.

  9. Kev writes:

    Lee,

    Excellent bit of code and nicely solves the problem of not having wsdl.exe available.

    Cheers
    Kev

  10. Hinek writes:

    Instead of this script one can now use the New-WebServiceProxy CMDlet.

    i.e. $listsProxy = New-WebServiceProxy “http://sharepoint/_vti_bin/Lists.asmx” -UseDefaultCredential

  11. Lee Holmes writes:

    Good point, Hinek, thank you. This post was written during the V1 PowerShell Cookbook. As might be no surprise, it was useful and we officially added it (in cmdlet form) in PowerShell V2.

  12. Irfan writes:

    How can i work with custom headers? My web service has a custom security header which i need to fill before calling any method.

    123

  13. Calling a Webservice from PowerShell | Precision Computing – Powershell writes:

    […] Source: Calling a Webservice from PowerShell | Precision Computing […]

Leave a Reply