Although SOAPProxy is a stand-alone solution to improving SOAP over PowerShell, it was developed as part of a greater problem that is discussed in Introduction to posts about JSONPath, SOAP and Amadeus with PowerShell.
When using New-WebServiceProxy, PowerShell will download the API's WSDL and use it to generate types for the proxy's interface, data contracts and headers. This is all achieved by leveraging the
System.Web.Services assembly which in .NET is the workhorse for the ASMX, a technology rather old. ASMX was the predecessor of WCF and is also known for supporting SOAP 1.1.
What is not clear to most, is that the generated types reside within an in-memory assembly. Let's imagine that we create a proxy against a SOAP interface containing one operation
DoSomething that accepts a parameter of type
DoSomethingRequest and returns an output of type
DoSomethingResponse are the data contracts.
$proxy1=New-WebServiceProxy -Uri $uriDoSomething -Namespace Example
-Namespace parameter is optional and when not specified then it gets a random value from the cmdlet. To help simplify this post, we'll keep using the parameter with value
Example, so all mentioned namespaces are rooted with
When the proxy is created, the following types should be available
DosomethingResponse reference other complex types in the WSDL, then those types will also be created within the
Example namespace. The expectation is that the following code fragment is enough to initialize an instance of
Example.DoSomethingRequest and use it to invoke the
$request=[Example.DoSomethingRequest]::new() $request.Property1="something" $response=$proxy.DoSomething($request)
All the types are part of an in-memory assembly that has a random name. To help this post, we will assume that the assembly is named
5wx5pbrx, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null.
In the meantime, if the
$proxy=New-WebServiceProxy -Uri $uriDoSomething -Namespace Example is executed again then problems start surfacing because the new execution creates yet another set of the same types but in a different assembly e.g.
4qodbdsw, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null. The two invocations would have resulted in following set of types:
|Invocation||Assembly Qualified Name|
For those who are not proficient with .NET, the type name is not enough to identity a type it in the runtime. Instead the full name of the assembly is also used to produce what you would consider a unique identifier of a type also known as Assembly Qualified Name.
$proxy1 expects as a parameter an instance of type
Example.DoSomethingRequest, 5wx5pbrx, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null and
Example.DoSomethingRequest, 4qodbdsw, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null. The problem is that when code like
[Example.DoSomethingRequest]::new() is executed then it is unclear which exact type was instantiated because we didn't specify the assembly qualified name and this is a scenario that is not typical within the .NET runtime. This can lead to situations where types from the
5wx5pbrx assembly would be used with types from the
4qodbdsw assembly, which leads to errors that don't make sense.
The SOAPProxy PowerShell module helps address this problem.
||Initializes a proxy from a URI. A proxy can be marked as Default and be acquired without explicit mention of the URI.|
||Finds and returns a proxy initialized by
||List each operation available on the SOAP proxy with the expected Request types and returned Response type.|
||Instantiates a request object expected by an operation in a proxy.|
||Helps with understanding how the request and response types of an operation are structured. This is effectively a redirection to
The module's main functionality is to wrap the
New-WebServiceProxy and make sure to invoke it only when necessary. The functionality is supported by
Initialize-SOAPProxy and internally, the cmdlet maintains a sort of proxy cache within the global variables. When the parameters of
Initialize-SOAPProxy match an existing proxy, then
New-WebServiceProxy is not executed and the one from memory is used. The condition to match an existing proxy is the
-Uri is also used by the
Get-SOAPProxy cmdlet to retrieve a proxy for a given URI. This is not only faster, it also helps avoid the mixup with the assemblies.
Initialize-SOAPProxy can also leverage the global variables to track a default proxy. When a proxy is created as default, then all cmdlets in the module can be used without specifying the
-URI parameter. This is a very common case because usually a script targets one endpoint and can be marked as default. Then, the rest of the script assumes that there is a default proxy which makes the code cleaner and easier to read. It increases also its maintainability.
For example, imagine a script that targets the SOAP endpoints specified in the variables
$uri2. In the following example, the proxy for
$uri1 is marked as the default one.
# Initialize for $uri1 Initialize-SOAPProxy -URI $uri1 -AsDefault # Initialize for $uri2 Initialize-SOAPProxy -URI $uri2
To retrieve the proxy for
$uri1, we can invoke
Get-Proxy implying the default proxy or by specifying the
# Get default proxy for uri1 $proxy1=Get-SOAPProxy # Get proxy for uri1 $proxy1=Get-SOAPProxy -Uri $uri1
To retrieve the proxy for
$uri2, we need to be explicit and specify the
# Get proxy for uri2 $proxy2=Get-SOAPProxy -URI uri2
For the rest of the examples, we will assume that a default proxy is created like this
Initialize-SOAPProxy -URI $uri -AsDefault.
With the SOAPProxy module depending on New-WebServiceProxy, it means that PowerShell versions higher than 5.1 are not supported. Unfortunately, New-WebServiceProxy is ASMX era technology and is powered by the
System.Web.Services assembly. Therefore, porting to PowerShell Core seems very unlucky because the assembly needs to be ported to .NET Core and that will probably never happen. ASMX is old technology and the successor is WCF but even that is not 100% supported by the .NET Core and the team has no plans to do better based on their most recent roadmap announcement. Regardless, I've created another issue on the PowerShell repository requesting SOAP support from PowerShell. In my comment, it becomes clear why there is much uncertainty if we will ever get cross-platform support for SOAP in PowerShell.
The most unfortunate side effect of this limitations is that scripts depending on the SOAPProxy module cannot be executed on
- Azure Function with PowerShell.
- Non Windows platforms.
The SOAPProxy module offers some additional cmdlets to help with using the proxy in a scripting environment. All examples assume that a default proxy is initialized with
Get-SOAPProxyInfo will list the operation along with their input and output types. For example
C:> Get-SOAPProxyInfo Operation RequestType ResponseType --------- ----------- ------------ DoSomething Example.DoSomethingRequest Example.DoSomethingResponse
To create an instance of
Example.DoSomethingRequest the module offers the
New-SOAPProxyRequest that will create one from a proxy by specifying the operation name.
New-SOAPProxyRequest -Operation DoSomething
New-SOAPProxyRequest can also work with soap proxies created outside the preview of the SOAPProxy module. This is possible because SOAPProxy manages proxies created by New-WebServiceProxy. All following code is valid and utilizes a proxy created normally
$proxy=New-WebServiceProxy -Uri $uri Get-SOAPProxyInfo -Proxy $proxy $proxy|Get-SOAPProxyInfo New-SOAPProxyRequest -Operation DoSomething -Proxy $proxy $proxy|New-SOAPProxyRequest -Operation DoSomething
Often the request and response objects are very complicated, referencing other composite types and arrays. Without code visibility, typical with a scripting environment, this becomes very difficult and requires lots of trial and error. JSONPath offers functionality which will be discussed in another post dedicated to JSONPath. SOAPProxy offers the
Trace-SOAPProxy with two options:
- A list of JSONPath like expressions that help understand the data contract relationships.
- A code that sets a value to every possible JSONPath expression. As a developer, you can then keep the lines that you are interested to form a valid request.
For the following examples, we'll use the
PNR_Retrieve operation from the Amadeus API
Trace-SOAPProxy -Proxy $doSomethingProxy -Operation PNR_Retrieve -Request outputs
settings.printer.identifierDetail.name="String" settings.printer.identifierDetail.network="String" settings.printer.office="String" settings.printer.teletypeAddress="String" settings.options="String" retrievalFacts.retrieve.type="String" retrievalFacts.retrieve.service="String" retrievalFacts.retrieve.tattoo="String" retrievalFacts.retrieve.office="String" retrievalFacts.retrieve.targetSystem="String" retrievalFacts.retrieve.option1="String" retrievalFacts.retrieve.option2="String" retrievalFacts.reservationOrProfileIdentifier.companyId="String" retrievalFacts.reservationOrProfileIdentifier.controlNumber="String" retrievalFacts.reservationOrProfileIdentifier.controlType="String" retrievalFacts.personalFacts.travellerInformation.traveller.surname="String" retrievalFacts.personalFacts.travellerInformation.passenger.firstName="String" retrievalFacts.personalFacts.productInformation.product.depDate="String" retrievalFacts.personalFacts.productInformation.product.depTime="String" retrievalFacts.personalFacts.productInformation.product.arrDate="String" retrievalFacts.personalFacts.productInformation.boardpointDetail.cityCode="String" retrievalFacts.personalFacts.productInformation.offpointDetail.cityCode="String" retrievalFacts.personalFacts.productInformation.company.code="String" retrievalFacts.personalFacts.productInformation.productDetails.identification="String" retrievalFacts.personalFacts.productInformation.productDetails.subtype="String" retrievalFacts.personalFacts.ticket.airline="String" retrievalFacts.personalFacts.ticket.ticketNumber="String" retrievalFacts.frequentFlyer.frequentTraveller.companyId="String" retrievalFacts.frequentFlyer.frequentTraveller.membershipNumber="String" retrievalFacts.accounting.account.number="String"
Trace-SOAPProxy -Proxy $doSomethingProxy -Operation PNR_Retrieve -Request -RenderCode outputs
$requestPNR_Retrieve=New-SOAPProxyRequest -Operation "PNR_Retrieve" $requestPNR_Retrieve=$requestPNR_Retrieve | Set-JSONPath -Path "settings.printer.identifierDetail.name" -Value "String" -PassThru | Set-JSONPath -Path "settings.printer.identifierDetail.network" -Value "String" -PassThru | Set-JSONPath -Path "settings.printer.office" -Value "String" -PassThru | Set-JSONPath -Path "settings.printer.teletypeAddress" -Value "String" -PassThru | Set-JSONPath -Path "settings.options" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.retrieve.type" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.retrieve.service" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.retrieve.tattoo" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.retrieve.office" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.retrieve.targetSystem" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.retrieve.option1" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.retrieve.option2" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.reservationOrProfileIdentifier.companyId" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.reservationOrProfileIdentifier.controlNumber" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.reservationOrProfileIdentifier.controlType" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.travellerInformation.traveller.surname" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.travellerInformation.passenger.firstName" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.productInformation.product.depDate" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.productInformation.product.depTime" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.productInformation.product.arrDate" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.productInformation.boardpointDetail.cityCode" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.productInformation.offpointDetail.cityCode" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.productInformation.company.code" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.productInformation.productDetails.identification" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.productInformation.productDetails.subtype" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.ticket.airline" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.personalFacts.ticket.ticketNumber" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.frequentFlyer.frequentTraveller.companyId" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.frequentFlyer.frequentTraveller.membershipNumber" -Value "String" -PassThru | Set-JSONPath -Path "retrievalFacts.accounting.account.number" -Value "String"