loading...

Improved SOAP proxies management in PowerShell

sarafian profile image Alex Sarafian Updated on ・8 min read

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.

Under the hood of New-WebServiceProxy

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. DoSomethingRequest and DoSomethingResponse are the data contracts.

$proxy1=New-WebServiceProxy -Uri $uriDoSomething -Namespace Example

The -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 Example.

When the proxy is created, the following types should be available

  • Example.DoSomethingRequest
  • Example.DosomethingResponse

If DoSomethingRequest or 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 DoSomething operation.

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

The problem

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
1st Example.DoSomethingRequest, 5wx5pbrx, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
1st Example.DosomethingResponse, 5wx5pbrx, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
2nd Example.DoSomethingRequest, 4qodbdsw, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
2nd Example.DosomethingResponse, 4qodbdsw, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

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.
{: .notice--tip}

$proxy1 expects as a parameter an instance of type Example.DoSomethingRequest, 5wx5pbrx, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null and $proxy2 expects 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.

SOAPProxy

Cmdlet Function
Initialize-SOAPProxy Initializes a proxy from a URI. A proxy can be marked as Default and be acquired without explicit mention of the URI.
Get-SOAPProxy Finds and returns a proxy initialized by Initialize-SOAPProxy. When the URI is not specified, then the default one is returned.
Get-SOAPProxyInfo List each operation available on the SOAP proxy with the expected Request types and returned Response type.
New-SOAPProxyRequest Instantiates a request object expected by an operation in a proxy.
Trace-SOAPProxy Helps with understanding how the request and response types of an operation are structured. This is effectively a redirection to Trace-JSONPath from the JSONPath module.

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

The 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 $uri1 and $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 -URI parameter.


# 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 -URI parameter.

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

PowerShell Core not supported

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.

Working with the soap proxy

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 Initialize-SOAPProxy.

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

Both Get-SOAPProxyInfo and 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

Debugging and tracing the proxies

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[0]="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[0].companyId="String"
retrievalFacts.reservationOrProfileIdentifier[0].controlNumber="String"
retrievalFacts.reservationOrProfileIdentifier[0].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[0]" -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[0].companyId" -Value "String" -PassThru |
    Set-JSONPath -Path "retrievalFacts.reservationOrProfileIdentifier[0].controlNumber" -Value "String" -PassThru |
    Set-JSONPath -Path "retrievalFacts.reservationOrProfileIdentifier[0].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"

Posted on by:

sarafian profile

Alex Sarafian

@sarafian

Senior generalist with technology

Discussion

pic
Editor guide