Last active
September 18, 2025 18:21
-
-
Save gerneio/57b1705037ee0fdde72751f35891d554 to your computer and use it in GitHub Desktop.
ReportingServicesTools Support PS Core/Linux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # https://github.com/microsoft/ReportingServicesTools/issues/239#issuecomment-2742133163 | |
| # .NET Core version of `svcutil` for converting WSDL to C# clients | |
| if (!(Get-Package "dotnet-svcutil" -RequiredVersion 8.0.0 -ErrorAction SilentlyContinue)) { | |
| Install-Package -Name "dotnet-svcutil" -Scope CurrentUser -RequiredVersion 8.0.0 -Source "https://www.nuget.org/api/v2" -Force | |
| } | |
| function New-WebServiceProxyStub { | |
| # mimic `New-WebServiceProxy` param sets | |
| [CmdletBinding(DefaultParameterSetName = "NoCredentials")] | |
| param ( | |
| [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "NoCredentials")] | |
| [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "Credential")] | |
| [Parameter(Position = 0, Mandatory = $true, ParameterSetName = "UseDefaultCredential")] | |
| [Alias("WL", "WSDL", "Path")] | |
| [System.Uri]$Uri, | |
| [Parameter(Position = 1, Mandatory = $false, ParameterSetName = "NoCredentials")] | |
| [Parameter(Position = 1, Mandatory = $false, ParameterSetName = "Credential")] | |
| [Parameter(Position = 1, Mandatory = $false, ParameterSetName = "UseDefaultCredential")] | |
| [Alias("FileName", "FN")] | |
| [System.String]$Class, | |
| [Parameter(Position = 2, Mandatory = $false, ParameterSetName = "NoCredentials")] | |
| [Parameter(Position = 2, Mandatory = $false, ParameterSetName = "Credential")] | |
| [Parameter(Position = 2, Mandatory = $false, ParameterSetName = "UseDefaultCredential")] | |
| [Alias("NS")] | |
| [System.String]$Namespace, | |
| [Parameter(Position = [int]::MinValue, Mandatory = $false, ParameterSetName = "Credential")] | |
| [Alias("Cred")] | |
| [System.Management.Automation.PSCredential]$Credential, | |
| [Parameter(Position = [int]::MinValue, Mandatory = $false, ParameterSetName = "UseDefaultCredential")] | |
| [Alias("UDC")] | |
| [System.Management.Automation.SwitchParameter]$UseDefaultCredential | |
| ) | |
| Write-Verbose "New-WebServiceProxyStub $args $($PSCmdlet.ParameterSetName)" | |
| #region Retrieve WSDL | |
| $reqArgs = @{} | |
| if ($Uri.Scheme -eq "http") { | |
| if ($PSVersionTable.PSEdition -eq "Core") { | |
| $reqArgs.AllowUnencryptedAuthentication = $true | |
| } | |
| } | |
| switch ($PSCmdlet.ParameterSetName) { | |
| "Credential" { $reqArgs.Credential = $Credential; break } | |
| "UseDefaultCredential" { $reqArgs.UseDefaultCredential = $true; break } | |
| "NoCredentials" { break } | |
| Default { throw "Unsupported" } | |
| } | |
| $wsdlPath = [system.IO.Path]::GetTempFileName() + ".wsdl" | |
| Invoke-WebRequest $Uri -SessionVariable websession -OutFile $wsdlPath @reqArgs | |
| $svcNode = Select-Xml -Path $wsdlPath -XPath "//wsdl:service" -Namespace @{ "wsdl" = "http://schemas.xmlsoap.org/wsdl/" } | |
| if (!$svcNode -or !($svcName = $svcNode.Node.name)) { throw "Unable to determine service name" } | |
| Write-Verbose "$wsdlPath $svcName" | |
| #endregion | |
| $svcNamespace = "WebService" | |
| $clientTypeName = "$svcNamespace.$svcName`SoapClient" | |
| $assm = [AppDomain]::CurrentDomain.GetAssemblies() | ? { $_.GetType($clientTypeName, $false, $true) } | |
| # Generate & load type into memory, if not already loaded (TODO: should it still be loaded anyway in case there are WS changes?) | |
| if (!$assm) { | |
| #region Execute `dotnet-svcutil`, generate C# client code, and load as in memory type(s) | |
| $tempPath = [System.IO.Path]::GetTempPath() | |
| $generatedClientCodeFile = [system.IO.Path]::GetRandomFileName(); | |
| $generatedClientCodePath = "$tempPath$generatedClientCodeFile.cs" | |
| $svcutilArgs = $wsdlPath, | |
| "--toolContext", "Infrastructure", | |
| "--verbosity", "Silent", # TODO: check VerbosePreference | |
| "--noLogo","--NoTelemetry", "--NoProjectUpdates", | |
| "--outputDir", $tempPath, | |
| "--outputFile", $generatedClientCodeFile, | |
| "--targetFramework", "netstandard2.0", # required to remove `dotnet` CLI tool dependency | |
| "--syncOnly", | |
| "--namespace", "*,$svcNamespace"; | |
| $svcutilDllFolder = Get-Item ((Get-Package "dotnet-svcutil" -RequiredVersion 8.0.0).Source + "\..\tools\net9.0\any") | |
| if ($PSVersionTable.PSEdition -eq "Core") { | |
| Add-Type -Path "$svcutilDllFolder\dotnet-svcutil-lib.dll" | |
| [Microsoft.Tools.ServiceModel.Svcutil.Tool]::Main($svcutilArgs) | Out-Null | |
| # PS Core requires more assembly references than Windows PS | |
| Add-Type -TypeDefinition (Get-Content $generatedClientCodePath -Raw) -IgnoreWarnings -ReferencedAssemblies "netstandard","System.Runtime.Serialization.Xml", | |
| "System.Xml.ReaderWriter","System.Xml.XmlSerializer","System.Private.ServiceModel","Microsoft.Bcl.AsyncInterfaces" | |
| } else { | |
| # TODO: Under Windows PS, since we can't load .net core DLL we require dotnet CLI to be installed, however if we can find a way execute the DLL w/o it, | |
| # would fully remove that dependency. Perhaps we can just fallback to `svcutil.exe`, if installed? Or just return the default `New-WebServiceProxy` | |
| # implementation in the first place. | |
| dotnet "$svcutilDllFolder\dotnet-svcutil.dll" $svcutilArgs | Write-Verbose | |
| Add-Type -TypeDefinition (Get-Content $generatedClientCodePath -Raw) -ReferencedAssemblies "System.ServiceModel","System.Xml","System.Runtime.Serialization" | |
| } | |
| Write-Verbose "Generated Code: $generatedClientCodePath" | |
| #endregion | |
| } | |
| #region Configure Soap WS Client | |
| $ep = [System.ServiceModel.EndpointAddress]::new($Uri) | |
| # TODO: Make certain options configurable | |
| if ($Uri.Scheme -eq "http") { | |
| $bind = [System.ServiceModel.BasicHttpBinding]::new() | |
| $bind.Security.Transport.ClientCredentialType = "Ntlm" | |
| $bind.Security.Mode = "TransportCredentialOnly" | |
| $bind.MaxReceivedMessageSize = [int]::MaxValue | |
| } else { | |
| $bind = [System.ServiceModel.BasicHttpsBinding]::new() | |
| $bind.Security.Transport.ClientCredentialType = "Ntlm" | |
| $bind.Security.Mode = "Transport" | |
| $bind.MaxReceivedMessageSize = [int]::MaxValue | |
| } | |
| $client = New-Object $clientTypeName -ArgumentList $bind, $ep | |
| $client.ClientCredentials.Windows.AllowedImpersonationLevel = "Impersonation" | |
| switch ($PSCmdlet.ParameterSetName) { | |
| "Credential" { $client.ClientCredentials.Windows.ClientCredential = $Credential; break } | |
| "UseDefaultCredential" { break } | |
| "NoCredentials" { break } | |
| Default { throw "Unsupported" } | |
| } | |
| #endregion | |
| #region Create proxy stub | |
| $proxyStub = @{ | |
| Client = $client | |
| WebSession = $websession | |
| } | |
| #region Create method stubs (might be SSRS WS specfic) | |
| $client.GetType().GetMethods() | ? { $_.ReturnParameter.ParameterType.Name -eq "ServerInfoHeader" } | % { | |
| $methodName = $_.Name | |
| $sc = [scriptblock]{ | |
| # TODO: we're relying on user input to determine [ref] usage, but perhaps we should prefer to look at the current method definition | |
| $refStartIndex = [Array]::FindIndex($args, [predicate[object]]{ $args[0] -is [ref] }) | |
| if ($refStartIndex -eq -1) { | |
| $inParams = $args | |
| $outParams = @() | |
| } elseif ($refStartIndex -eq 0) { | |
| $inParams = @() | |
| $outParams = $args | |
| } else { | |
| $inParams = $args[0..($refStartIndex-1)] | |
| $outParams = $args[$refStartIndex..($args.Length)] | |
| } | |
| $returnParam = [ref] $null | |
| # Param order: | |
| $params = @( | |
| $null; # 1) `TrustedUserHeader` should be set to NULL | |
| $inParams; # 2) All `SoapIn` params | |
| $returnParam; # 3) First `SoapOut` param is set as our return variable | |
| $outParams; # 4) Remaining `SoapOut` params | |
| ) | |
| $this.Client.$methodName.Invoke($params) | Out-Null | |
| # return first out variable | |
| $returnParam.Value | |
| }.GetNewClosure() # closure retains method name | |
| $proxyStub | Add-Member ScriptMethod -Name $methodName -Value $sc -Force | |
| } | |
| # Override proxy stub type w/ client type for now (some external modules seem to rely on it) | |
| $proxyStub | Add-Member ScriptMethod -Name "GetType" -Value { $this.Client.GetType() } -Force | |
| #endregion | |
| return $proxyStub | |
| } | |
| # Override depreciated `New-WebServiceProxy` | |
| # Set-Alias "New-WebServiceProxy" -Value New-WebServiceProxyStub # alias works just as well | |
| function New-WebServiceProxy { | |
| New-WebServiceProxyStub @args | |
| } | |
| Export-ModuleMember -Function New-WebServiceProxy |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| `New-WebServiceProxy` is no longer supported in PS core (see: https://github.com/PowerShell/PowerShell/issues/9838) | |
| and therefore the existing `ReportingServicesTools` won't work either (excluding REST methods). | |
| Option 1) Use `Import-Module ReportingServicesTools -UseWindowsPowerShell` (see: https://github.com/microsoft/ReportingServicesTools/issues/239#issuecomment-595115438) | |
| Option 2) If you're executing under linux OR just prefer to stay within PS Core (i.e. perf benefits + latest feature + no serialization boundary), | |
| then the below script might be helpful to you: | |
| * Uses `dotnet-svcutil` to generate WSDL-to-C# Client at runtime | |
| * WSDL is retireved in a similiar manner as previous proxy (by making authenticated WS call at runtime) | |
| * Generated C# client code is loaded in memory | |
| * Since [ReportingService2010SoapClient] method definitions are not 1-to-1 with existing WSDL code gen (methods tend to have extra `TrustedUserHeader` param & return as out param instead), | |
| we must re-implement `New-WebServiceProxy` by creating stub method wrappers targeting the underlying client methods. This has the benefit of allowing the `ReportingServicesTools` PS module | |
| to "just work", with the disadvantage of having to manually stub out each method translation. Below code demonstrates how to stub this out dynamically using reflection API + scriptblock closures. | |
| * This script has been tested to work on PS Core, Windows Powershell, as well as Ubuntu 24.04 (had 401 auth errors on older Ubuntu versions) | |
| Most of this logic would probably apply to other ASMX/WSDL web services, minus the SSRS WS specific stuff | |
| #> | |
| Import-Module "./New-WebServiceProxyStub.psm1" -Force # -Verbose | |
| if (!(Get-Module ReportingServicesTools -ListAvailable)) { | |
| Install-Module -Name ReportingServicesTools -Force | |
| } | |
| $reportServer = "http://localhost/reportserver"; | |
| $reportUser = "**********"; | |
| $reportPassword = '**********'; | |
| $reportDomain = "**********"; | |
| $reportFolder = "**********"; | |
| $password = $reportPassword | ConvertTo-SecureString -AsPlainText -Force | |
| $credential = New-Object System.Management.Automation.PSCredential("$reportDomain\$reportUser", $password) | |
| # New-WebServiceProxy -Uri "$reportServer/ReportService2010.asmx" -Credential $credential -Verbose | |
| $proxy = New-RsWebServiceProxy -ReportServerUri $reportServer -Credential $credential | |
| $items = Get-RsFolderContent -RsFolder "/$reportFolder" -Proxy $proxy | |
| $items.Length | |
| # Out-RsFolderContent -Proxy $proxy -RsFolder "/$reportFolder" -Recurse -Destination "$reportFolder" -Verbose | |
| # Get-RsSubscription "/$reportFolder" -Proxy $proxy | Export-RsSubscriptionXml -Path "$reportFolder-Subscriptions.xml" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment