Calling a Webservice from PowerShell
Wednesday, 28 February 2007
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.]


Subscribe to this blog.
No. 1 — March 1st, 2007 at 3:14 am
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.
No. 2 — March 1st, 2007 at 3:22 am
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();
}
No. 3 — March 19th, 2007 at 2:05 pm
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
No. 4 — March 19th, 2007 at 5:20 pm
Hi Tom;
Try this: http://www.leeholmes.com/blog/ConvertingCToPowerShell.aspx
No. 5 — March 19th, 2007 at 6:31 pm
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
No. 6 — September 26th, 2007 at 12:24 am
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. :)
No. 7 — August 13th, 2008 at 9:08 am
Absolutely brilliant code! So useful. Thanx a big bunch!
No. 8 — October 5th, 2009 at 3:41 pm
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.
No. 9 — September 13th, 2010 at 3:33 pm
Lee,
Excellent bit of code and nicely solves the problem of not having wsdl.exe available.
Cheers
Kev
No. 10 — January 26th, 2011 at 8:23 am
Instead of this script one can now use the New-WebServiceProxy CMDlet.
i.e. $listsProxy = New-WebServiceProxy “http://sharepoint/_vti_bin/Lists.asmx” -UseDefaultCredential
No. 11 — January 28th, 2011 at 12:35 am
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.
No. 12 — July 7th, 2011 at 9:43 am
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