PowerShell Cookbook

Search

Categories

 

On this page

Archive

Blogroll

Disclaimer
I work for Microsoft.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

RSS 2.0 | Atom 1.0 | CDF

Send mail to the author(s) E-mail

Total Posts: 257
This Year: 8
This Month: 2
This Week: 0
Comments: 785

Sign In

 Tuesday, February 27, 2007
Wednesday, February 28, 2007 5:17:41 AM (Pacific Standard Time, UTC-08:00) ( )

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.]

Thursday, March 01, 2007 3:14:22 AM (Pacific Standard Time, UTC-08:00)
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.
Thursday, March 01, 2007 3:22:33 AM (Pacific Standard Time, UTC-08:00)
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();
}
Monday, March 19, 2007 2:05:47 PM (Pacific Daylight Time, UTC-07:00)
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
Tom
Monday, March 19, 2007 5:20:32 PM (Pacific Daylight Time, UTC-07:00)
Hi Tom;

Try this: http://www.leeholmes.com/blog/ConvertingCToPowerShell.aspx
Monday, March 19, 2007 6:31:51 PM (Pacific Daylight Time, UTC-07:00)
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

Tom
Wednesday, September 26, 2007 12:24:22 AM (Pacific Daylight Time, UTC-07:00)
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. :)
Keith J. Farmer
Wednesday, August 13, 2008 9:08:47 AM (Pacific Daylight Time, UTC-07:00)
Absolutely brilliant code! So useful. Thanx a big bunch!
Monday, October 05, 2009 3:41:26 PM (Pacific Daylight Time, UTC-07:00)
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.
David Choi
Name
E-mail
Home page

Comment (Some html is allowed: b, blockquote@cite, em, i, strike, strong, sub, super, u)  

Enter the code shown (prevents robots):