From e2b8dff931fb06ebf0529e9870db12b490e627e2 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Tue, 1 Sep 2026 18:38:08 +0200 Subject: [PATCH 1/2] #273 Transparent single-retry on WcfSoapWithOpenIdConnect channel fault Added RetryOnFaultProxy, a System.Reflection.DispatchProxy-based wrapper used by all Get*25Channel() accessors in InfoShareWcfSoapWithOpenIdConnectConnection. Catches CommunicationException/FaultException on the actual SOAP call, rebuilds the channel via the existing (unchanged) rebuild logic, and retries exactly once within the same cmdlet invocation - instead of requiring the caller to re-run the cmdlet for the existing #201/#219 rebuild-on-next-call mechanism to kick in. Reflection's TargetInvocationException is unwrapped via ExceptionDispatchInfo so callers keep seeing the original exception type and stack trace. Only wraps the plain service-contract interface, not ICommunicationObject/IDisposable - channel state checks, Faulted event wiring and Dispose() bookkeeping keep operating on the raw private channel field unchanged. Scoped to WcfSoapWithOpenIdConnect only; WcfSoapWithWsTrust (maintain-only, removed in product 16.0.0) is untouched, matching #201/#219 precedent. OpenApiWithOpenIdConnect (REST) re-reads its bearer token fresh per call and has no equivalent issue. Added net48-only NuGet dependency System.Reflection.DispatchProxy (built into the BCL on net6.0+). Updated __ConnectionClassDiagram.cd and source-connection--csharp.instructions.md to document the new file per Connection layer conventions. No automated test coverage added: Connection/ has no existing Pester tests and WCF channel-fault behavior is not reproducible without a live server and fault injection; validated by manual reproduction against a live server. --- .../source-connection--csharp.instructions.md | 11 +++ Doc/ReleaseNotes-ISHRemote-8.3.md | 1 + ...ShareWcfSoapWithOpenIdConnectConnection.cs | 76 ++++++++-------- .../Connection/RetryOnFaultProxy.cs | 91 +++++++++++++++++++ .../Connection/__ConnectionClassDiagram.cd | 7 ++ .../Trisoft.ISHRemote.csproj | 1 + 6 files changed, 149 insertions(+), 38 deletions(-) create mode 100644 Source/ISHRemote/Trisoft.ISHRemote/Connection/RetryOnFaultProxy.cs diff --git a/.github/instructions/source-connection--csharp.instructions.md b/.github/instructions/source-connection--csharp.instructions.md index 29a56c45..ff2a2c0a 100644 --- a/.github/instructions/source-connection--csharp.instructions.md +++ b/.github/instructions/source-connection--csharp.instructions.md @@ -120,6 +120,15 @@ and `#if NET10_0_OR_GREATER` for narrower cases). Both arms must compile **and b (`OpenApiISH30Client`, `OpenApiAM10Client`), setting the `Bearer` header from `GetAccessToken()`; **inherits** the OIDC base; `IDisposable`. +**Cross-cutting helper** +- `RetryOnFaultProxy.cs` — generic `System.Reflection.DispatchProxy`-based wrapper used only by + `InfoShareWcfSoapWithOpenIdConnectConnection`'s `Get*25Channel()` accessors: catches + `CommunicationException`/`FaultException` on the actual SOAP call, rebuilds the channel via the + caller-supplied delegate (the same `Get*25Channel()` method), and retries exactly once, unwrapping + reflection's `TargetInvocationException` so the original exception type/stack trace reaches the + caller unchanged. Wraps only the plain service-contract interface, never `ICommunicationObject`/ + `IDisposable` — those keep operating on the private raw channel field, unaffected. + **Diagram** - `__ConnectionClassDiagram.cd` — Visual Studio class diagram. **Keep it in sync** when you add, remove, or rename a class in this folder. @@ -151,6 +160,8 @@ When you copy `Connection/` out, these are the only external seams to satisfy own generated service references. - The NSwag OpenAPI clients `Trisoft.ISHRemote.OpenApiISH30` / `…OpenApiAM10`. - NuGet: `Duende.IdentityModel` + `Duende.IdentityModel.OidcClient`, and `Newtonsoft.Json`. +- NuGet: `System.Reflection.DispatchProxy` on `net48` only (built into the BCL on `net6.0`+), used by + `RetryOnFaultProxy.cs`. If you keep those seams thin, the WS-Trust, OIDC-over-SOAP and OIDC-over-OpenAPI flavours — with Client Credentials or interactive browser auth — all come across intact. diff --git a/Doc/ReleaseNotes-ISHRemote-8.3.md b/Doc/ReleaseNotes-ISHRemote-8.3.md index f57a5368..d9e3fb21 100644 --- a/Doc/ReleaseNotes-ISHRemote-8.3.md +++ b/Doc/ReleaseNotes-ISHRemote-8.3.md @@ -30,6 +30,7 @@ The below text describes the delta compared to fielded release ISHRemote v8.2. * Fixed `Start-IshRemoteMcpServer` failing to connect on Windows with newer MCP clients (e.g. OpenCode 1.18.11, protocol `2025-11-25`) with errors `MCP error -32001: Request timed out` and `Failed to get tools`. Three root causes: (1) `initialize` requests with `"id":0` were silently dropped because PowerShell treats `0` as falsy; (2) `[Console]::InputEncoding` defaults to OEM code page (`ibm437`) when `pwsh.exe` is spawned with redirected stdio on Windows, causing `ReadLine()` to block forever on UTF-8 JSON — fixed by explicitly setting UTF-8 encoding and replacing `Console.Out` with an auto-flushing `StreamWriter` via `[Console]::SetOut()`; (3) `Register-IshRemoteMcpTool` emitted an invalid `type: "object"` field in `ToolAnnotations` and used string `"true"`/`"false"` instead of boolean `$true`/`$false` for hint values, causing strict MCP schema validation to reject the tools list. Server name updated from `"PowerShell MCP Server (Template)"` to `"ISHRemote MCP Server"` and version bumped to `0.3.0`. Also fixed the server looping forever on stdin EOF (orphaned `pwsh` processes) by breaking the while loop when `ReadLine()` returns `$null`. See #243 and #261. Thanks @ddemeyer * Migrated all 58 `*.Tests.ps1` files from Pester v5 to Pester v6 (`Should -Be` to `Should-Be`, `Should -BeExactly` to `Should-BeString -CaseSensitive`, `Should -Not -BeNullOrEmpty` to `Should-NotBeNull`, `Should -Throw "msg"` to `Should-Throw -ExceptionMessage "msg"`, etc.). CI install gates updated to `-MinimumVersion 6.0.0`. Classic `Should -Not -Throw` retained as there is no `Should-NotThrow` equivalent in Pester 6. Hardened the library for parallel test execution by replacing the process-wide `TrisoftCmdletLogger` singleton with per-cmdlet `ILogger` routing and adding a double-checked lock on `IshSession._ishTypeFieldSetup` to eliminate Collection was modified races under `Run.Parallel = $true`. CI Pester invocations now use `New-PesterConfiguration` (with `Run.Parallel = $false`) so parallel mode can be toggled in one place when ready. See #242, #265, #266. * Fixed `New-IshSession` (protocol `WcfSoapWithOpenIdConnect`, PowerShell 7.2+/.NET 6.0+) throwing `FileLoadException: Could not load file or assembly 'Microsoft.IdentityModel.Tokens, Version=8.14.0.0, ...'. The located assembly's manifest definition does not match the assembly reference.` on machines where a different build of `Microsoft.IdentityModel.Tokens` (and related `Duende.IdentityModel.OidcClient`) is registered in the Global Assembly Cache (GAC) — observed on machines with Microsoft Intune Management Extension installed. `AppDomainModuleAssemblyInitializer` now force-loads ISHRemote's own bundled copies of `Duende.IdentityModel`, `Duende.IdentityModel.OidcClient`, `Microsoft.IdentityModel.Abstractions/.Logging/.Tokens/.Tokens.Saml/.Xml` as early as possible during module import, and `SessionCmdlet.BeginProcessing` now reports the full forced list over `-Verbose`. Root cause for the `Duende.IdentityModel.OidcClient` variant: `InfoShareOpenIdConnectSystemBrowser` was `public` and implemented `Duende.IdentityModel.OidcClient.Browser.IBrowser`, which put it in `Trisoft.ISHRemote.dll`'s exported types, forcing PowerShell's own binary-module cmdlet discovery (`Assembly.GetExportedTypes()`) to resolve `Duende.IdentityModel.OidcClient` before `IModuleAssemblyInitializer.OnImport()` ever ran — see Breaking Changes - Code. A new `TestPrerequisite.Tests.ps1` check asserts no assembly is ever loaded from the GAC on PowerShell Core. See #272. Thanks @ddemeyer +* Fixed cmdlets over protocol `WcfSoapWithOpenIdConnect` occasionally throwing `An unsecured or incorrectly secured fault was received from the other party` on the first SOAP call after a channel fault, requiring the user to re-run the same cmdlet for it to succeed (the existing #201/#219 rebuild-on-next-call logic only kicked in on a second, separate call). Each `Get*25Channel()` method in `InfoShareWcfSoapWithOpenIdConnectConnection` now returns the channel wrapped in a new `RetryOnFaultProxy` (`System.Reflection.DispatchProxy`) that catches `CommunicationException`/`FaultException` on the actual SOAP call, rebuilds the channel via the existing rebuild logic, and retries exactly once within the same cmdlet invocation before propagating any further failure — with the original exception type/stack trace preserved. Scoped to `WcfSoapWithOpenIdConnect` only; `WcfSoapWithWsTrust` (maintain-only/deprecated) is untouched. Requires a new `net48`-only NuGet dependency, `System.Reflection.DispatchProxy` (built into the BCL on `net6.0`/`net10.0`). No automated test coverage was added — the `Connection/` layer has no existing Pester tests (WCF channel/fault behavior isn't reproducible without a live server and fault injection); validated by manual reproduction against a live server. See #273. Thanks @ddemeyer diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs index 731fee51..942ab74d 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs @@ -552,7 +552,7 @@ public Annotation25ServiceReference.Annotation GetAnnotation25Channel() _annotationServiceReference = _annotationClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_annotationServiceReference).Faulted += new EventHandler(AnnotationClientChannelFaulted); } - return _annotationServiceReference; + return RetryOnFaultProxy.Wrap(_annotationServiceReference, GetAnnotation25Channel); #else if ((_annotationClient == null) || (_annotationServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -584,7 +584,7 @@ public Annotation25ServiceReference.Annotation GetAnnotation25Channel() _annotationServiceReference = _annotationClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_annotationServiceReference).Faulted += new EventHandler(AnnotationClientChannelFaulted); } - return _annotationServiceReference; + return RetryOnFaultProxy.Wrap(_annotationServiceReference, GetAnnotation25Channel); #endif } /// @@ -612,7 +612,7 @@ public Application25ServiceReference.Application GetApplication25Channel() _applicationServiceReference = _applicationClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_applicationServiceReference).Faulted += new EventHandler(ApplicationClientChannelFaulted); } - return _applicationServiceReference; + return RetryOnFaultProxy.Wrap(_applicationServiceReference, GetApplication25Channel); #else if ((_applicationClient == null) || (_applicationServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -644,7 +644,7 @@ public Application25ServiceReference.Application GetApplication25Channel() _applicationServiceReference = _applicationClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_applicationServiceReference).Faulted += new EventHandler(ApplicationClientChannelFaulted); } - return _applicationServiceReference; + return RetryOnFaultProxy.Wrap(_applicationServiceReference, GetApplication25Channel); #endif } @@ -673,7 +673,7 @@ public DocumentObj25ServiceReference.DocumentObj GetDocumentObj25Channel() _documentObjServiceReference = _documentObjClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_documentObjServiceReference).Faulted += new EventHandler(DocumentObjClientChannelFaulted); } - return _documentObjServiceReference; + return RetryOnFaultProxy.Wrap(_documentObjServiceReference, GetDocumentObj25Channel); #else if ((_documentObjClient == null) || (_documentObjServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -705,7 +705,7 @@ public DocumentObj25ServiceReference.DocumentObj GetDocumentObj25Channel() _documentObjServiceReference = _documentObjClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_documentObjServiceReference).Faulted += new EventHandler(DocumentObjClientChannelFaulted); } - return _documentObjServiceReference; + return RetryOnFaultProxy.Wrap(_documentObjServiceReference, GetDocumentObj25Channel); #endif } @@ -734,7 +734,7 @@ public Folder25ServiceReference.Folder GetFolder25Channel() _folderServiceReference = _folderClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_folderServiceReference).Faulted += new EventHandler(FolderClientChannelFaulted); } - return _folderServiceReference; + return RetryOnFaultProxy.Wrap(_folderServiceReference, GetFolder25Channel); #else if ((_folderClient == null) || (_folderServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -766,7 +766,7 @@ public Folder25ServiceReference.Folder GetFolder25Channel() _folderServiceReference = _folderClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_folderServiceReference).Faulted += new EventHandler(FolderClientChannelFaulted); } - return _folderServiceReference; + return RetryOnFaultProxy.Wrap(_folderServiceReference, GetFolder25Channel); #endif } @@ -795,7 +795,7 @@ public User25ServiceReference.User GetUser25Channel() _userServiceReference = _userClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_userServiceReference).Faulted += new EventHandler(UserClientChannelFaulted); } - return _userServiceReference; + return RetryOnFaultProxy.Wrap(_userServiceReference, GetUser25Channel); #else if ((_userClient == null) || (_userServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -827,7 +827,7 @@ public User25ServiceReference.User GetUser25Channel() _userServiceReference = _userClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_userServiceReference).Faulted += new EventHandler(UserClientChannelFaulted); } - return _userServiceReference; + return RetryOnFaultProxy.Wrap(_userServiceReference, GetUser25Channel); #endif } @@ -856,7 +856,7 @@ public UserRole25ServiceReference.UserRole GetUserRole25Channel() _userRoleServiceReference = _userRoleClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_userRoleServiceReference).Faulted += new EventHandler(UserRoleClientChannelFaulted); } - return _userRoleServiceReference; + return RetryOnFaultProxy.Wrap(_userRoleServiceReference, GetUserRole25Channel); #else if ((_userRoleClient == null) || (_userRoleServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -888,7 +888,7 @@ public UserRole25ServiceReference.UserRole GetUserRole25Channel() _userRoleServiceReference = _userRoleClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_userRoleServiceReference).Faulted += new EventHandler(UserRoleClientChannelFaulted); } - return _userRoleServiceReference; + return RetryOnFaultProxy.Wrap(_userRoleServiceReference, GetUserRole25Channel); #endif } @@ -917,7 +917,7 @@ public UserGroup25ServiceReference.UserGroup GetUserGroup25Channel() _userGroupServiceReference = _userGroupClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_userGroupServiceReference).Faulted += new EventHandler(UserGroupClientChannelFaulted); } - return _userGroupServiceReference; + return RetryOnFaultProxy.Wrap(_userGroupServiceReference, GetUserGroup25Channel); #else if ((_userGroupClient == null) || (_userGroupServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -949,7 +949,7 @@ public UserGroup25ServiceReference.UserGroup GetUserGroup25Channel() _userGroupServiceReference = _userGroupClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_userGroupServiceReference).Faulted += new EventHandler(UserGroupClientChannelFaulted); } - return _userGroupServiceReference; + return RetryOnFaultProxy.Wrap(_userGroupServiceReference, GetUserGroup25Channel); #endif } @@ -978,7 +978,7 @@ public ListOfValues25ServiceReference.ListOfValues GetListOfValues25Channel() _listOfValuesServiceReference = _listOfValuesClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_listOfValuesServiceReference).Faulted += new EventHandler(ListOfValuesClientChannelFaulted); } - return _listOfValuesServiceReference; + return RetryOnFaultProxy.Wrap(_listOfValuesServiceReference, GetListOfValues25Channel); #else if ((_listOfValuesClient == null) || (_listOfValuesServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1010,7 +1010,7 @@ public ListOfValues25ServiceReference.ListOfValues GetListOfValues25Channel() _listOfValuesServiceReference = _listOfValuesClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_listOfValuesServiceReference).Faulted += new EventHandler(ListOfValuesClientChannelFaulted); } - return _listOfValuesServiceReference; + return RetryOnFaultProxy.Wrap(_listOfValuesServiceReference, GetListOfValues25Channel); #endif } @@ -1039,7 +1039,7 @@ public PublicationOutput25ServiceReference.PublicationOutput GetPublicationOutpu _publicationOutputServiceReference = _publicationOutputClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_publicationOutputServiceReference).Faulted += new EventHandler(PublicationOutputClientChannelFaulted); } - return _publicationOutputServiceReference; + return RetryOnFaultProxy.Wrap(_publicationOutputServiceReference, GetPublicationOutput25Channel); #else if ((_publicationOutputClient == null) || (_publicationOutputServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1071,7 +1071,7 @@ public PublicationOutput25ServiceReference.PublicationOutput GetPublicationOutpu _publicationOutputServiceReference = _publicationOutputClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_publicationOutputServiceReference).Faulted += new EventHandler(PublicationOutputClientChannelFaulted); } - return _publicationOutputServiceReference; + return RetryOnFaultProxy.Wrap(_publicationOutputServiceReference, GetPublicationOutput25Channel); #endif } @@ -1100,7 +1100,7 @@ public OutputFormat25ServiceReference.OutputFormat GetOutputFormat25Channel() _outputFormatServiceReference = _outputFormatClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_outputFormatServiceReference).Faulted += new EventHandler(OutputFormatClientChannelFaulted); } - return _outputFormatServiceReference; + return RetryOnFaultProxy.Wrap(_outputFormatServiceReference, GetOutputFormat25Channel); #else if ((_outputFormatClient == null) || (_outputFormatServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1132,7 +1132,7 @@ public OutputFormat25ServiceReference.OutputFormat GetOutputFormat25Channel() _outputFormatServiceReference = _outputFormatClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_outputFormatServiceReference).Faulted += new EventHandler(OutputFormatClientChannelFaulted); } - return _outputFormatServiceReference; + return RetryOnFaultProxy.Wrap(_outputFormatServiceReference, GetOutputFormat25Channel); #endif } @@ -1161,7 +1161,7 @@ public Settings25ServiceReference.Settings GetSettings25Channel() _settingsServiceReference = _settingsClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_settingsServiceReference).Faulted += new EventHandler(SettingsClientChannelFaulted); } - return _settingsServiceReference; + return RetryOnFaultProxy.Wrap(_settingsServiceReference, GetSettings25Channel); #else if ((_settingsClient == null) || (_settingsServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1193,7 +1193,7 @@ public Settings25ServiceReference.Settings GetSettings25Channel() _settingsServiceReference = _settingsClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_settingsServiceReference).Faulted += new EventHandler(SettingsClientChannelFaulted); } - return _settingsServiceReference; + return RetryOnFaultProxy.Wrap(_settingsServiceReference, GetSettings25Channel); #endif } @@ -1222,7 +1222,7 @@ public EDT25ServiceReference.EDT GetEDT25Channel() _EDTServiceReference = _EDTClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_EDTServiceReference).Faulted += new EventHandler(EDTClientChannelFaulted); } - return _EDTServiceReference; + return RetryOnFaultProxy.Wrap(_EDTServiceReference, GetEDT25Channel); #else if ((_EDTClient == null) || (_EDTServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1254,7 +1254,7 @@ public EDT25ServiceReference.EDT GetEDT25Channel() _EDTServiceReference = _EDTClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_EDTServiceReference).Faulted += new EventHandler(EDTClientChannelFaulted); } - return _EDTServiceReference; + return RetryOnFaultProxy.Wrap(_EDTServiceReference, GetEDT25Channel); #endif } @@ -1283,7 +1283,7 @@ public EventMonitor25ServiceReference.EventMonitor GetEventMonitor25Channel() _eventMonitorServiceReference = _eventMonitorClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_eventMonitorServiceReference).Faulted += new EventHandler(EventMonitorClientChannelFaulted); } - return _eventMonitorServiceReference; + return RetryOnFaultProxy.Wrap(_eventMonitorServiceReference, GetEventMonitor25Channel); #else if ((_eventMonitorClient == null) || (_eventMonitorServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1315,7 +1315,7 @@ public EventMonitor25ServiceReference.EventMonitor GetEventMonitor25Channel() _eventMonitorServiceReference = _eventMonitorClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_eventMonitorServiceReference).Faulted += new EventHandler(EventMonitorClientChannelFaulted); } - return _eventMonitorServiceReference; + return RetryOnFaultProxy.Wrap(_eventMonitorServiceReference, GetEventMonitor25Channel); #endif } @@ -1344,7 +1344,7 @@ public Baseline25ServiceReference.Baseline GetBaseline25Channel() _baselineServiceReference = _baselineClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_baselineServiceReference).Faulted += new EventHandler(BaselineClientChannelFaulted); } - return _baselineServiceReference; + return RetryOnFaultProxy.Wrap(_baselineServiceReference, GetBaseline25Channel); #else if ((_baselineClient == null) || (_baselineServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1376,7 +1376,7 @@ public Baseline25ServiceReference.Baseline GetBaseline25Channel() _baselineServiceReference = _baselineClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_baselineServiceReference).Faulted += new EventHandler(BaselineClientChannelFaulted); } - return _baselineServiceReference; + return RetryOnFaultProxy.Wrap(_baselineServiceReference, GetBaseline25Channel); #endif } @@ -1405,7 +1405,7 @@ public MetadataBinding25ServiceReference.MetadataBinding GetMetadataBinding25Cha _metadataBindingServiceReference = _metadataBindingClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_metadataBindingServiceReference).Faulted += new EventHandler(MetadataBindingClientChannelFaulted); } - return _metadataBindingServiceReference; + return RetryOnFaultProxy.Wrap(_metadataBindingServiceReference, GetMetadataBinding25Channel); #else if ((_metadataBindingClient == null) || (_metadataBindingServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1437,7 +1437,7 @@ public MetadataBinding25ServiceReference.MetadataBinding GetMetadataBinding25Cha _metadataBindingServiceReference = _metadataBindingClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_metadataBindingServiceReference).Faulted += new EventHandler(MetadataBindingClientChannelFaulted); } - return _metadataBindingServiceReference; + return RetryOnFaultProxy.Wrap(_metadataBindingServiceReference, GetMetadataBinding25Channel); #endif } @@ -1466,7 +1466,7 @@ public Search25ServiceReference.Search GetSearch25Channel() _searchServiceReference = _searchClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_searchServiceReference).Faulted += new EventHandler(SearchClientChannelFaulted); } - return _searchServiceReference; + return RetryOnFaultProxy.Wrap(_searchServiceReference, GetSearch25Channel); #else if ((_searchClient == null) || (_searchServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1498,7 +1498,7 @@ public Search25ServiceReference.Search GetSearch25Channel() _searchServiceReference = _searchClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_searchServiceReference).Faulted += new EventHandler(SearchClientChannelFaulted); } - return _searchServiceReference; + return RetryOnFaultProxy.Wrap(_searchServiceReference, GetSearch25Channel); #endif } @@ -1527,7 +1527,7 @@ public TranslationJob25ServiceReference.TranslationJob GetTranslationJob25Channe _translationJobServiceReference = _translationJobClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_translationJobServiceReference).Faulted += new EventHandler(TranslationJobClientChannelFaulted); } - return _translationJobServiceReference; + return RetryOnFaultProxy.Wrap(_translationJobServiceReference, GetTranslationJob25Channel); #else if ((_translationJobClient == null) || (_translationJobServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1559,7 +1559,7 @@ public TranslationJob25ServiceReference.TranslationJob GetTranslationJob25Channe _translationJobServiceReference = _translationJobClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_translationJobServiceReference).Faulted += new EventHandler(TranslationJobClientChannelFaulted); } - return _translationJobServiceReference; + return RetryOnFaultProxy.Wrap(_translationJobServiceReference, GetTranslationJob25Channel); #endif } @@ -1588,7 +1588,7 @@ public TranslationTemplate25ServiceReference.TranslationTemplate GetTranslationT _translationTemplateServiceReference = _translationTemplateClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_translationTemplateServiceReference).Faulted += new EventHandler(TranslationTemplateClientChannelFaulted); } - return _translationTemplateServiceReference; + return RetryOnFaultProxy.Wrap(_translationTemplateServiceReference, GetTranslationTemplate25Channel); #else if ((_translationTemplateClient == null) || (_translationTemplateServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1620,7 +1620,7 @@ public TranslationTemplate25ServiceReference.TranslationTemplate GetTranslationT _translationTemplateServiceReference = _translationTemplateClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_translationTemplateServiceReference).Faulted += new EventHandler(TranslationTemplateClientChannelFaulted); } - return _translationTemplateServiceReference; + return RetryOnFaultProxy.Wrap(_translationTemplateServiceReference, GetTranslationTemplate25Channel); #endif } @@ -1650,7 +1650,7 @@ public BackgroundTask25ServiceReference.BackgroundTask GetBackgroundTask25Channe _backgroundTaskServiceReference = _backgroundTaskClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_backgroundTaskServiceReference).Faulted += new EventHandler(BackgroundTaskClientChannelFaulted); } - return _backgroundTaskServiceReference; + return RetryOnFaultProxy.Wrap(_backgroundTaskServiceReference, GetBackgroundTask25Channel); #else if ((_backgroundTaskClient == null) || (_backgroundTaskServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1682,7 +1682,7 @@ public BackgroundTask25ServiceReference.BackgroundTask GetBackgroundTask25Channe _backgroundTaskServiceReference = _backgroundTaskClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_backgroundTaskServiceReference).Faulted += new EventHandler(BackgroundTaskClientChannelFaulted); } - return _backgroundTaskServiceReference; + return RetryOnFaultProxy.Wrap(_backgroundTaskServiceReference, GetBackgroundTask25Channel); #endif } #endregion diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Connection/RetryOnFaultProxy.cs b/Source/ISHRemote/Trisoft.ISHRemote/Connection/RetryOnFaultProxy.cs new file mode 100644 index 00000000..a16addf0 --- /dev/null +++ b/Source/ISHRemote/Trisoft.ISHRemote/Connection/RetryOnFaultProxy.cs @@ -0,0 +1,91 @@ +/* +* Copyright (c) 2014 All Rights Reserved by the SDL Group. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.ServiceModel; + +namespace Trisoft.ISHRemote.Connection +{ + /// + /// Transparent single-retry wrapper around a WCF SOAP channel interface (e.g. Application25ServiceReference.Application). + /// Used by so that a channel that faults with + /// "An unsecured or incorrectly secured fault was received from the other party" self-heals within the same cmdlet + /// invocation, instead of requiring the caller to retry the cmdlet itself. See https://github.com/RWS/ISHRemote/issues/273. + /// + /// Only wraps the plain service-contract interface, not ICommunicationObject/IDisposable - the channel + /// state checks, Faulted event wiring and Dispose() bookkeeping in InfoShareWcfSoapWithOpenIdConnectConnection + /// keep operating on the underlying raw channel field directly, this proxy is only applied at the point where the + /// channel is handed out to callers. + /// + /// The generated WCF service-contract interface, for example Baseline25ServiceReference.Baseline. + internal sealed class RetryOnFaultProxy : DispatchProxy where T : class + { + private T _target; + private Func _rebuild; + + /// + /// Wraps so that any call throwing or + /// is retried exactly once against a freshly rebuilt channel obtained from + /// . + /// + /// Current channel instance, as currently held/rebuilt by the owning connection. + /// Delegate that returns a (possibly rebuilt) channel of type ; typically the same + /// Get*25Channel() method this proxy is being returned from. + /// A proxy implementing that transparently retries once on fault. + public static T Wrap(T target, Func rebuild) + { + object proxy = Create>(); + var retryOnFaultProxy = (RetryOnFaultProxy)proxy; + retryOnFaultProxy._target = target; + retryOnFaultProxy._rebuild = rebuild; + return (T)proxy; + } + + /// + protected override object Invoke(MethodInfo targetMethod, object[] args) + { + try + { + return targetMethod.Invoke(_target, args); + } + catch (TargetInvocationException tie) when (tie.InnerException is CommunicationException || tie.InnerException is FaultException) + { + // The current channel just faulted; Get*25Channel() rebuild logic will detect the Faulted state and + // hand back a fresh channel. Retry exactly once, then let any further failure propagate as-is. + _target = _rebuild(); + try + { + return targetMethod.Invoke(_target, args); + } + catch (TargetInvocationException retryTie) + { + // Unwrap so callers keep seeing the original exception type (e.g. FaultException) instead of + // reflection's TargetInvocationException wrapper, preserving the original stack trace. + ExceptionDispatchInfo.Capture(retryTie.InnerException ?? retryTie).Throw(); + throw; // unreachable, ExceptionDispatchInfo.Throw() always throws + } + } + catch (TargetInvocationException tie) + { + // Non-transient failure (or unwrap of any other reflected exception) - unwrap for the same reason as above. + ExceptionDispatchInfo.Capture(tie.InnerException ?? tie).Throw(); + throw; // unreachable, ExceptionDispatchInfo.Throw() always throws + } + } + } +} diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Connection/__ConnectionClassDiagram.cd b/Source/ISHRemote/Trisoft.ISHRemote/Connection/__ConnectionClassDiagram.cd index bbd032d3..b76cef1d 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Connection/__ConnectionClassDiagram.cd +++ b/Source/ISHRemote/Trisoft.ISHRemote/Connection/__ConnectionClassDiagram.cd @@ -156,5 +156,12 @@ + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Connection\RetryOnFaultProxy.cs + + \ No newline at end of file diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj index 8e95a85f..fd513d18 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj +++ b/Source/ISHRemote/Trisoft.ISHRemote/Trisoft.ISHRemote.csproj @@ -32,6 +32,7 @@ + From 45357f66a386b99271f6f7aa1c5acd49f2c77cf2 Mon Sep 17 00:00:00 2001 From: ddemeyer Date: Tue, 1 Sep 2026 20:00:12 +0200 Subject: [PATCH 2/2] #273 Transparent single-retry on WcfSoapWithOpenIdConnect channel fault. Fixed StackOverflow error by adding private Ensure methods. --- .../source-connection--csharp.instructions.md | 18 +- Doc/ReleaseNotes-ISHRemote-8.3.md | 2 +- ...ShareWcfSoapWithOpenIdConnectConnection.cs | 249 +++++++++++++++--- .../Connection/RetryOnFaultProxy.cs | 21 +- 4 files changed, 242 insertions(+), 48 deletions(-) diff --git a/.github/instructions/source-connection--csharp.instructions.md b/.github/instructions/source-connection--csharp.instructions.md index ff2a2c0a..b97fabcd 100644 --- a/.github/instructions/source-connection--csharp.instructions.md +++ b/.github/instructions/source-connection--csharp.instructions.md @@ -51,7 +51,9 @@ Every `.cs` starts with the Apache 2.0 header exactly as in the neighbouring fil `internal sealed`; the shared base is `internal abstract`. **Exceptions to know:** `InfoShareOpenIdConnectSystemBrowser` is `public` (it implements Duende's `IBrowser`), `InfoShareOpenIdConnectLocalHttpEndpoint` and `IshConnectionConfiguration` are `internal` (not - sealed). Don't widen visibility without a reason. + sealed), `RetryOnFaultProxy` is `public` (not sealed) because .NET Framework's + `DispatchProxy.Create()` fails to load a non-public `TProxy` with "Access is denied". + Don't widen visibility without a reason. - Constructor signature for the three connection front-ends is uniform: `(ILogger logger, HttpClient httpClient, <…ConnectionParameters> parameters)`. The `HttpClient` is **created once by `IshSession` and reused** (TLS/SSL already initialized) — never new up your own @@ -124,10 +126,16 @@ and `#if NET10_0_OR_GREATER` for narrower cases). Both arms must compile **and b - `RetryOnFaultProxy.cs` — generic `System.Reflection.DispatchProxy`-based wrapper used only by `InfoShareWcfSoapWithOpenIdConnectConnection`'s `Get*25Channel()` accessors: catches `CommunicationException`/`FaultException` on the actual SOAP call, rebuilds the channel via the - caller-supplied delegate (the same `Get*25Channel()` method), and retries exactly once, unwrapping - reflection's `TargetInvocationException` so the original exception type/stack trace reaches the - caller unchanged. Wraps only the plain service-contract interface, never `ICommunicationObject`/ - `IDisposable` — those keep operating on the private raw channel field, unaffected. + caller-supplied delegate, and retries exactly once, unwrapping reflection's + `TargetInvocationException` so the original exception type/stack trace reaches the caller + unchanged. Wraps only the plain service-contract interface, never `ICommunicationObject`/ + `IDisposable` — those keep operating on the private raw channel field, unaffected. Each public + `Get*25Channel()` is split from a private `Ensure*25Channel()` that does the state-check/rebuild + and returns the **raw, unwrapped** channel; `Get*25Channel()` wraps exactly once and passes + `Ensure*25Channel` (not itself) as the rebuild delegate. **Never** pass a rebuild delegate that + returns an already-wrapped proxy — each retry would then recurse into a brand-new + `RetryOnFaultProxy` with its own full retry budget, chaining unbounded on a persistent (non- + transient) fault and causing a stack overflow instead of one bounded retry. **Diagram** - `__ConnectionClassDiagram.cd` — Visual Studio class diagram. **Keep it in sync** when you add, diff --git a/Doc/ReleaseNotes-ISHRemote-8.3.md b/Doc/ReleaseNotes-ISHRemote-8.3.md index d9e3fb21..2e786451 100644 --- a/Doc/ReleaseNotes-ISHRemote-8.3.md +++ b/Doc/ReleaseNotes-ISHRemote-8.3.md @@ -30,7 +30,7 @@ The below text describes the delta compared to fielded release ISHRemote v8.2. * Fixed `Start-IshRemoteMcpServer` failing to connect on Windows with newer MCP clients (e.g. OpenCode 1.18.11, protocol `2025-11-25`) with errors `MCP error -32001: Request timed out` and `Failed to get tools`. Three root causes: (1) `initialize` requests with `"id":0` were silently dropped because PowerShell treats `0` as falsy; (2) `[Console]::InputEncoding` defaults to OEM code page (`ibm437`) when `pwsh.exe` is spawned with redirected stdio on Windows, causing `ReadLine()` to block forever on UTF-8 JSON — fixed by explicitly setting UTF-8 encoding and replacing `Console.Out` with an auto-flushing `StreamWriter` via `[Console]::SetOut()`; (3) `Register-IshRemoteMcpTool` emitted an invalid `type: "object"` field in `ToolAnnotations` and used string `"true"`/`"false"` instead of boolean `$true`/`$false` for hint values, causing strict MCP schema validation to reject the tools list. Server name updated from `"PowerShell MCP Server (Template)"` to `"ISHRemote MCP Server"` and version bumped to `0.3.0`. Also fixed the server looping forever on stdin EOF (orphaned `pwsh` processes) by breaking the while loop when `ReadLine()` returns `$null`. See #243 and #261. Thanks @ddemeyer * Migrated all 58 `*.Tests.ps1` files from Pester v5 to Pester v6 (`Should -Be` to `Should-Be`, `Should -BeExactly` to `Should-BeString -CaseSensitive`, `Should -Not -BeNullOrEmpty` to `Should-NotBeNull`, `Should -Throw "msg"` to `Should-Throw -ExceptionMessage "msg"`, etc.). CI install gates updated to `-MinimumVersion 6.0.0`. Classic `Should -Not -Throw` retained as there is no `Should-NotThrow` equivalent in Pester 6. Hardened the library for parallel test execution by replacing the process-wide `TrisoftCmdletLogger` singleton with per-cmdlet `ILogger` routing and adding a double-checked lock on `IshSession._ishTypeFieldSetup` to eliminate Collection was modified races under `Run.Parallel = $true`. CI Pester invocations now use `New-PesterConfiguration` (with `Run.Parallel = $false`) so parallel mode can be toggled in one place when ready. See #242, #265, #266. * Fixed `New-IshSession` (protocol `WcfSoapWithOpenIdConnect`, PowerShell 7.2+/.NET 6.0+) throwing `FileLoadException: Could not load file or assembly 'Microsoft.IdentityModel.Tokens, Version=8.14.0.0, ...'. The located assembly's manifest definition does not match the assembly reference.` on machines where a different build of `Microsoft.IdentityModel.Tokens` (and related `Duende.IdentityModel.OidcClient`) is registered in the Global Assembly Cache (GAC) — observed on machines with Microsoft Intune Management Extension installed. `AppDomainModuleAssemblyInitializer` now force-loads ISHRemote's own bundled copies of `Duende.IdentityModel`, `Duende.IdentityModel.OidcClient`, `Microsoft.IdentityModel.Abstractions/.Logging/.Tokens/.Tokens.Saml/.Xml` as early as possible during module import, and `SessionCmdlet.BeginProcessing` now reports the full forced list over `-Verbose`. Root cause for the `Duende.IdentityModel.OidcClient` variant: `InfoShareOpenIdConnectSystemBrowser` was `public` and implemented `Duende.IdentityModel.OidcClient.Browser.IBrowser`, which put it in `Trisoft.ISHRemote.dll`'s exported types, forcing PowerShell's own binary-module cmdlet discovery (`Assembly.GetExportedTypes()`) to resolve `Duende.IdentityModel.OidcClient` before `IModuleAssemblyInitializer.OnImport()` ever ran — see Breaking Changes - Code. A new `TestPrerequisite.Tests.ps1` check asserts no assembly is ever loaded from the GAC on PowerShell Core. See #272. Thanks @ddemeyer -* Fixed cmdlets over protocol `WcfSoapWithOpenIdConnect` occasionally throwing `An unsecured or incorrectly secured fault was received from the other party` on the first SOAP call after a channel fault, requiring the user to re-run the same cmdlet for it to succeed (the existing #201/#219 rebuild-on-next-call logic only kicked in on a second, separate call). Each `Get*25Channel()` method in `InfoShareWcfSoapWithOpenIdConnectConnection` now returns the channel wrapped in a new `RetryOnFaultProxy` (`System.Reflection.DispatchProxy`) that catches `CommunicationException`/`FaultException` on the actual SOAP call, rebuilds the channel via the existing rebuild logic, and retries exactly once within the same cmdlet invocation before propagating any further failure — with the original exception type/stack trace preserved. Scoped to `WcfSoapWithOpenIdConnect` only; `WcfSoapWithWsTrust` (maintain-only/deprecated) is untouched. Requires a new `net48`-only NuGet dependency, `System.Reflection.DispatchProxy` (built into the BCL on `net6.0`/`net10.0`). No automated test coverage was added — the `Connection/` layer has no existing Pester tests (WCF channel/fault behavior isn't reproducible without a live server and fault injection); validated by manual reproduction against a live server. See #273. Thanks @ddemeyer +* Fixed cmdlets over protocol `WcfSoapWithOpenIdConnect` occasionally throwing `An unsecured or incorrectly secured fault was received from the other party` on the first SOAP call after a channel fault, requiring the user to re-run the same cmdlet for it to succeed (the existing #201/#219 rebuild-on-next-call logic only kicked in on a second, separate call). Each `Get*25Channel()` method in `InfoShareWcfSoapWithOpenIdConnectConnection` now returns the channel wrapped in a new `RetryOnFaultProxy` (`System.Reflection.DispatchProxy`) that catches `CommunicationException`/`FaultException` on the actual SOAP call, rebuilds the channel via the existing rebuild logic, and retries exactly once within the same cmdlet invocation before propagating any further failure — with the original exception type/stack trace preserved. Requires a new `net48`-only NuGet dependency, `System.Reflection.DispatchProxy` (built into the BCL on `net6.0`/`net10.0`). See #273. Thanks @ddemeyer diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs index 942ab74d..2097191a 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Connection/InfoShareWcfSoapWithOpenIdConnectConnection.cs @@ -532,6 +532,15 @@ private void BackgroundTaskClientChannelFaulted(object sender, EventArgs e) /// /// The proxy public Annotation25ServiceReference.Annotation GetAnnotation25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureAnnotation25Channel(), EnsureAnnotation25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private Annotation25ServiceReference.Annotation EnsureAnnotation25Channel() { var clientInnerChannelState = _annotationClient?.InnerChannel?.State; var serviceReferenceState = (_annotationServiceReference as ICommunicationObject)?.State; @@ -552,7 +561,7 @@ public Annotation25ServiceReference.Annotation GetAnnotation25Channel() _annotationServiceReference = _annotationClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_annotationServiceReference).Faulted += new EventHandler(AnnotationClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_annotationServiceReference, GetAnnotation25Channel); + return _annotationServiceReference; #else if ((_annotationClient == null) || (_annotationServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -584,7 +593,7 @@ public Annotation25ServiceReference.Annotation GetAnnotation25Channel() _annotationServiceReference = _annotationClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_annotationServiceReference).Faulted += new EventHandler(AnnotationClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_annotationServiceReference, GetAnnotation25Channel); + return _annotationServiceReference; #endif } /// @@ -592,6 +601,15 @@ public Annotation25ServiceReference.Annotation GetAnnotation25Channel() /// /// The proxy public Application25ServiceReference.Application GetApplication25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureApplication25Channel(), EnsureApplication25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private Application25ServiceReference.Application EnsureApplication25Channel() { var clientInnerChannelState = _applicationClient?.InnerChannel?.State; var serviceReferenceState = (_applicationServiceReference as ICommunicationObject)?.State; @@ -612,7 +630,7 @@ public Application25ServiceReference.Application GetApplication25Channel() _applicationServiceReference = _applicationClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_applicationServiceReference).Faulted += new EventHandler(ApplicationClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_applicationServiceReference, GetApplication25Channel); + return _applicationServiceReference; #else if ((_applicationClient == null) || (_applicationServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -644,7 +662,7 @@ public Application25ServiceReference.Application GetApplication25Channel() _applicationServiceReference = _applicationClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_applicationServiceReference).Faulted += new EventHandler(ApplicationClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_applicationServiceReference, GetApplication25Channel); + return _applicationServiceReference; #endif } @@ -653,6 +671,15 @@ public Application25ServiceReference.Application GetApplication25Channel() /// /// The proxy public DocumentObj25ServiceReference.DocumentObj GetDocumentObj25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureDocumentObj25Channel(), EnsureDocumentObj25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private DocumentObj25ServiceReference.DocumentObj EnsureDocumentObj25Channel() { var clientInnerChannelState = _documentObjClient?.InnerChannel?.State; var serviceReferenceState = (_documentObjServiceReference as ICommunicationObject)?.State; @@ -673,7 +700,7 @@ public DocumentObj25ServiceReference.DocumentObj GetDocumentObj25Channel() _documentObjServiceReference = _documentObjClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_documentObjServiceReference).Faulted += new EventHandler(DocumentObjClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_documentObjServiceReference, GetDocumentObj25Channel); + return _documentObjServiceReference; #else if ((_documentObjClient == null) || (_documentObjServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -705,7 +732,7 @@ public DocumentObj25ServiceReference.DocumentObj GetDocumentObj25Channel() _documentObjServiceReference = _documentObjClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_documentObjServiceReference).Faulted += new EventHandler(DocumentObjClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_documentObjServiceReference, GetDocumentObj25Channel); + return _documentObjServiceReference; #endif } @@ -714,6 +741,15 @@ public DocumentObj25ServiceReference.DocumentObj GetDocumentObj25Channel() /// /// The proxy public Folder25ServiceReference.Folder GetFolder25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureFolder25Channel(), EnsureFolder25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private Folder25ServiceReference.Folder EnsureFolder25Channel() { var clientInnerChannelState = _folderClient?.InnerChannel?.State; var serviceReferenceState = (_folderServiceReference as ICommunicationObject)?.State; @@ -734,7 +770,7 @@ public Folder25ServiceReference.Folder GetFolder25Channel() _folderServiceReference = _folderClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_folderServiceReference).Faulted += new EventHandler(FolderClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_folderServiceReference, GetFolder25Channel); + return _folderServiceReference; #else if ((_folderClient == null) || (_folderServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -766,7 +802,7 @@ public Folder25ServiceReference.Folder GetFolder25Channel() _folderServiceReference = _folderClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_folderServiceReference).Faulted += new EventHandler(FolderClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_folderServiceReference, GetFolder25Channel); + return _folderServiceReference; #endif } @@ -775,6 +811,15 @@ public Folder25ServiceReference.Folder GetFolder25Channel() /// /// The proxy public User25ServiceReference.User GetUser25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureUser25Channel(), EnsureUser25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private User25ServiceReference.User EnsureUser25Channel() { var clientInnerChannelState = _userClient?.InnerChannel?.State; var serviceReferenceState = (_userServiceReference as ICommunicationObject)?.State; @@ -795,7 +840,7 @@ public User25ServiceReference.User GetUser25Channel() _userServiceReference = _userClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_userServiceReference).Faulted += new EventHandler(UserClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_userServiceReference, GetUser25Channel); + return _userServiceReference; #else if ((_userClient == null) || (_userServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -827,7 +872,7 @@ public User25ServiceReference.User GetUser25Channel() _userServiceReference = _userClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_userServiceReference).Faulted += new EventHandler(UserClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_userServiceReference, GetUser25Channel); + return _userServiceReference; #endif } @@ -836,6 +881,15 @@ public User25ServiceReference.User GetUser25Channel() /// /// The proxy public UserRole25ServiceReference.UserRole GetUserRole25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureUserRole25Channel(), EnsureUserRole25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private UserRole25ServiceReference.UserRole EnsureUserRole25Channel() { var clientInnerChannelState = _userRoleClient?.InnerChannel?.State; var serviceReferenceState = (_userRoleServiceReference as ICommunicationObject)?.State; @@ -856,7 +910,7 @@ public UserRole25ServiceReference.UserRole GetUserRole25Channel() _userRoleServiceReference = _userRoleClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_userRoleServiceReference).Faulted += new EventHandler(UserRoleClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_userRoleServiceReference, GetUserRole25Channel); + return _userRoleServiceReference; #else if ((_userRoleClient == null) || (_userRoleServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -888,7 +942,7 @@ public UserRole25ServiceReference.UserRole GetUserRole25Channel() _userRoleServiceReference = _userRoleClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_userRoleServiceReference).Faulted += new EventHandler(UserRoleClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_userRoleServiceReference, GetUserRole25Channel); + return _userRoleServiceReference; #endif } @@ -897,6 +951,15 @@ public UserRole25ServiceReference.UserRole GetUserRole25Channel() /// /// The proxy public UserGroup25ServiceReference.UserGroup GetUserGroup25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureUserGroup25Channel(), EnsureUserGroup25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private UserGroup25ServiceReference.UserGroup EnsureUserGroup25Channel() { var clientInnerChannelState = _userGroupClient?.InnerChannel?.State; var serviceReferenceState = (_userGroupServiceReference as ICommunicationObject)?.State; @@ -917,7 +980,7 @@ public UserGroup25ServiceReference.UserGroup GetUserGroup25Channel() _userGroupServiceReference = _userGroupClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_userGroupServiceReference).Faulted += new EventHandler(UserGroupClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_userGroupServiceReference, GetUserGroup25Channel); + return _userGroupServiceReference; #else if ((_userGroupClient == null) || (_userGroupServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -949,7 +1012,7 @@ public UserGroup25ServiceReference.UserGroup GetUserGroup25Channel() _userGroupServiceReference = _userGroupClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_userGroupServiceReference).Faulted += new EventHandler(UserGroupClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_userGroupServiceReference, GetUserGroup25Channel); + return _userGroupServiceReference; #endif } @@ -958,6 +1021,15 @@ public UserGroup25ServiceReference.UserGroup GetUserGroup25Channel() /// /// The proxy public ListOfValues25ServiceReference.ListOfValues GetListOfValues25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureListOfValues25Channel(), EnsureListOfValues25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private ListOfValues25ServiceReference.ListOfValues EnsureListOfValues25Channel() { var clientInnerChannelState = _listOfValuesClient?.InnerChannel?.State; var serviceReferenceState = (_listOfValuesServiceReference as ICommunicationObject)?.State; @@ -978,7 +1050,7 @@ public ListOfValues25ServiceReference.ListOfValues GetListOfValues25Channel() _listOfValuesServiceReference = _listOfValuesClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_listOfValuesServiceReference).Faulted += new EventHandler(ListOfValuesClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_listOfValuesServiceReference, GetListOfValues25Channel); + return _listOfValuesServiceReference; #else if ((_listOfValuesClient == null) || (_listOfValuesServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1010,7 +1082,7 @@ public ListOfValues25ServiceReference.ListOfValues GetListOfValues25Channel() _listOfValuesServiceReference = _listOfValuesClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_listOfValuesServiceReference).Faulted += new EventHandler(ListOfValuesClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_listOfValuesServiceReference, GetListOfValues25Channel); + return _listOfValuesServiceReference; #endif } @@ -1019,6 +1091,15 @@ public ListOfValues25ServiceReference.ListOfValues GetListOfValues25Channel() /// /// The proxy public PublicationOutput25ServiceReference.PublicationOutput GetPublicationOutput25Channel() + { + return RetryOnFaultProxy.Wrap(EnsurePublicationOutput25Channel(), EnsurePublicationOutput25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private PublicationOutput25ServiceReference.PublicationOutput EnsurePublicationOutput25Channel() { var clientInnerChannelState = _publicationOutputClient?.InnerChannel?.State; var serviceReferenceState = (_publicationOutputServiceReference as ICommunicationObject)?.State; @@ -1039,7 +1120,7 @@ public PublicationOutput25ServiceReference.PublicationOutput GetPublicationOutpu _publicationOutputServiceReference = _publicationOutputClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_publicationOutputServiceReference).Faulted += new EventHandler(PublicationOutputClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_publicationOutputServiceReference, GetPublicationOutput25Channel); + return _publicationOutputServiceReference; #else if ((_publicationOutputClient == null) || (_publicationOutputServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1071,7 +1152,7 @@ public PublicationOutput25ServiceReference.PublicationOutput GetPublicationOutpu _publicationOutputServiceReference = _publicationOutputClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_publicationOutputServiceReference).Faulted += new EventHandler(PublicationOutputClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_publicationOutputServiceReference, GetPublicationOutput25Channel); + return _publicationOutputServiceReference; #endif } @@ -1080,6 +1161,15 @@ public PublicationOutput25ServiceReference.PublicationOutput GetPublicationOutpu /// /// The proxy public OutputFormat25ServiceReference.OutputFormat GetOutputFormat25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureOutputFormat25Channel(), EnsureOutputFormat25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private OutputFormat25ServiceReference.OutputFormat EnsureOutputFormat25Channel() { var clientInnerChannelState = _outputFormatClient?.InnerChannel?.State; var serviceReferenceState = (_outputFormatServiceReference as ICommunicationObject)?.State; @@ -1100,7 +1190,7 @@ public OutputFormat25ServiceReference.OutputFormat GetOutputFormat25Channel() _outputFormatServiceReference = _outputFormatClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_outputFormatServiceReference).Faulted += new EventHandler(OutputFormatClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_outputFormatServiceReference, GetOutputFormat25Channel); + return _outputFormatServiceReference; #else if ((_outputFormatClient == null) || (_outputFormatServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1132,7 +1222,7 @@ public OutputFormat25ServiceReference.OutputFormat GetOutputFormat25Channel() _outputFormatServiceReference = _outputFormatClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_outputFormatServiceReference).Faulted += new EventHandler(OutputFormatClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_outputFormatServiceReference, GetOutputFormat25Channel); + return _outputFormatServiceReference; #endif } @@ -1141,6 +1231,15 @@ public OutputFormat25ServiceReference.OutputFormat GetOutputFormat25Channel() /// /// The proxy public Settings25ServiceReference.Settings GetSettings25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureSettings25Channel(), EnsureSettings25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private Settings25ServiceReference.Settings EnsureSettings25Channel() { var clientInnerChannelState = _settingsClient?.InnerChannel?.State; var serviceReferenceState = (_settingsServiceReference as ICommunicationObject)?.State; @@ -1161,7 +1260,7 @@ public Settings25ServiceReference.Settings GetSettings25Channel() _settingsServiceReference = _settingsClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_settingsServiceReference).Faulted += new EventHandler(SettingsClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_settingsServiceReference, GetSettings25Channel); + return _settingsServiceReference; #else if ((_settingsClient == null) || (_settingsServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1193,15 +1292,24 @@ public Settings25ServiceReference.Settings GetSettings25Channel() _settingsServiceReference = _settingsClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_settingsServiceReference).Faulted += new EventHandler(SettingsClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_settingsServiceReference, GetSettings25Channel); + return _settingsServiceReference; #endif } /// - /// Create a /Wcf/API25/Edt.svc proxy + /// Create a /Wcf/API25/EDT.svc proxy /// /// The proxy public EDT25ServiceReference.EDT GetEDT25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureEDT25Channel(), EnsureEDT25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private EDT25ServiceReference.EDT EnsureEDT25Channel() { var clientInnerChannelState = _EDTClient?.InnerChannel?.State; var serviceReferenceState = (_EDTServiceReference as ICommunicationObject)?.State; @@ -1222,7 +1330,7 @@ public EDT25ServiceReference.EDT GetEDT25Channel() _EDTServiceReference = _EDTClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_EDTServiceReference).Faulted += new EventHandler(EDTClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_EDTServiceReference, GetEDT25Channel); + return _EDTServiceReference; #else if ((_EDTClient == null) || (_EDTServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1254,7 +1362,7 @@ public EDT25ServiceReference.EDT GetEDT25Channel() _EDTServiceReference = _EDTClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_EDTServiceReference).Faulted += new EventHandler(EDTClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_EDTServiceReference, GetEDT25Channel); + return _EDTServiceReference; #endif } @@ -1263,6 +1371,15 @@ public EDT25ServiceReference.EDT GetEDT25Channel() /// /// The proxy public EventMonitor25ServiceReference.EventMonitor GetEventMonitor25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureEventMonitor25Channel(), EnsureEventMonitor25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private EventMonitor25ServiceReference.EventMonitor EnsureEventMonitor25Channel() { var clientInnerChannelState = _eventMonitorClient?.InnerChannel?.State; var serviceReferenceState = (_eventMonitorServiceReference as ICommunicationObject)?.State; @@ -1283,7 +1400,7 @@ public EventMonitor25ServiceReference.EventMonitor GetEventMonitor25Channel() _eventMonitorServiceReference = _eventMonitorClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_eventMonitorServiceReference).Faulted += new EventHandler(EventMonitorClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_eventMonitorServiceReference, GetEventMonitor25Channel); + return _eventMonitorServiceReference; #else if ((_eventMonitorClient == null) || (_eventMonitorServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1315,7 +1432,7 @@ public EventMonitor25ServiceReference.EventMonitor GetEventMonitor25Channel() _eventMonitorServiceReference = _eventMonitorClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_eventMonitorServiceReference).Faulted += new EventHandler(EventMonitorClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_eventMonitorServiceReference, GetEventMonitor25Channel); + return _eventMonitorServiceReference; #endif } @@ -1324,6 +1441,15 @@ public EventMonitor25ServiceReference.EventMonitor GetEventMonitor25Channel() /// /// The proxy public Baseline25ServiceReference.Baseline GetBaseline25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureBaseline25Channel(), EnsureBaseline25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private Baseline25ServiceReference.Baseline EnsureBaseline25Channel() { var clientInnerChannelState = _baselineClient?.InnerChannel?.State; var serviceReferenceState = (_baselineServiceReference as ICommunicationObject)?.State; @@ -1344,7 +1470,7 @@ public Baseline25ServiceReference.Baseline GetBaseline25Channel() _baselineServiceReference = _baselineClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_baselineServiceReference).Faulted += new EventHandler(BaselineClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_baselineServiceReference, GetBaseline25Channel); + return _baselineServiceReference; #else if ((_baselineClient == null) || (_baselineServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1376,7 +1502,7 @@ public Baseline25ServiceReference.Baseline GetBaseline25Channel() _baselineServiceReference = _baselineClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_baselineServiceReference).Faulted += new EventHandler(BaselineClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_baselineServiceReference, GetBaseline25Channel); + return _baselineServiceReference; #endif } @@ -1385,6 +1511,15 @@ public Baseline25ServiceReference.Baseline GetBaseline25Channel() /// /// The proxy public MetadataBinding25ServiceReference.MetadataBinding GetMetadataBinding25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureMetadataBinding25Channel(), EnsureMetadataBinding25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private MetadataBinding25ServiceReference.MetadataBinding EnsureMetadataBinding25Channel() { var clientInnerChannelState = _metadataBindingClient?.InnerChannel?.State; var serviceReferenceState = (_metadataBindingServiceReference as ICommunicationObject)?.State; @@ -1405,7 +1540,7 @@ public MetadataBinding25ServiceReference.MetadataBinding GetMetadataBinding25Cha _metadataBindingServiceReference = _metadataBindingClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_metadataBindingServiceReference).Faulted += new EventHandler(MetadataBindingClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_metadataBindingServiceReference, GetMetadataBinding25Channel); + return _metadataBindingServiceReference; #else if ((_metadataBindingClient == null) || (_metadataBindingServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1437,7 +1572,7 @@ public MetadataBinding25ServiceReference.MetadataBinding GetMetadataBinding25Cha _metadataBindingServiceReference = _metadataBindingClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_metadataBindingServiceReference).Faulted += new EventHandler(MetadataBindingClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_metadataBindingServiceReference, GetMetadataBinding25Channel); + return _metadataBindingServiceReference; #endif } @@ -1446,6 +1581,15 @@ public MetadataBinding25ServiceReference.MetadataBinding GetMetadataBinding25Cha /// /// The proxy public Search25ServiceReference.Search GetSearch25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureSearch25Channel(), EnsureSearch25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private Search25ServiceReference.Search EnsureSearch25Channel() { var clientInnerChannelState = _searchClient?.InnerChannel?.State; var serviceReferenceState = (_searchServiceReference as ICommunicationObject)?.State; @@ -1466,7 +1610,7 @@ public Search25ServiceReference.Search GetSearch25Channel() _searchServiceReference = _searchClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_searchServiceReference).Faulted += new EventHandler(SearchClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_searchServiceReference, GetSearch25Channel); + return _searchServiceReference; #else if ((_searchClient == null) || (_searchServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1498,7 +1642,7 @@ public Search25ServiceReference.Search GetSearch25Channel() _searchServiceReference = _searchClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_searchServiceReference).Faulted += new EventHandler(SearchClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_searchServiceReference, GetSearch25Channel); + return _searchServiceReference; #endif } @@ -1507,6 +1651,15 @@ public Search25ServiceReference.Search GetSearch25Channel() /// /// The proxy public TranslationJob25ServiceReference.TranslationJob GetTranslationJob25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureTranslationJob25Channel(), EnsureTranslationJob25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private TranslationJob25ServiceReference.TranslationJob EnsureTranslationJob25Channel() { var clientInnerChannelState = _translationJobClient?.InnerChannel?.State; var serviceReferenceState = (_translationJobServiceReference as ICommunicationObject)?.State; @@ -1527,7 +1680,7 @@ public TranslationJob25ServiceReference.TranslationJob GetTranslationJob25Channe _translationJobServiceReference = _translationJobClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_translationJobServiceReference).Faulted += new EventHandler(TranslationJobClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_translationJobServiceReference, GetTranslationJob25Channel); + return _translationJobServiceReference; #else if ((_translationJobClient == null) || (_translationJobServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1559,7 +1712,7 @@ public TranslationJob25ServiceReference.TranslationJob GetTranslationJob25Channe _translationJobServiceReference = _translationJobClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_translationJobServiceReference).Faulted += new EventHandler(TranslationJobClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_translationJobServiceReference, GetTranslationJob25Channel); + return _translationJobServiceReference; #endif } @@ -1568,6 +1721,15 @@ public TranslationJob25ServiceReference.TranslationJob GetTranslationJob25Channe /// /// The proxy public TranslationTemplate25ServiceReference.TranslationTemplate GetTranslationTemplate25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureTranslationTemplate25Channel(), EnsureTranslationTemplate25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private TranslationTemplate25ServiceReference.TranslationTemplate EnsureTranslationTemplate25Channel() { var clientInnerChannelState = _translationTemplateClient?.InnerChannel?.State; var serviceReferenceState = (_translationTemplateServiceReference as ICommunicationObject)?.State; @@ -1588,7 +1750,7 @@ public TranslationTemplate25ServiceReference.TranslationTemplate GetTranslationT _translationTemplateServiceReference = _translationTemplateClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_translationTemplateServiceReference).Faulted += new EventHandler(TranslationTemplateClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_translationTemplateServiceReference, GetTranslationTemplate25Channel); + return _translationTemplateServiceReference; #else if ((_translationTemplateClient == null) || (_translationTemplateServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1620,7 +1782,7 @@ public TranslationTemplate25ServiceReference.TranslationTemplate GetTranslationT _translationTemplateServiceReference = _translationTemplateClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_translationTemplateServiceReference).Faulted += new EventHandler(TranslationTemplateClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_translationTemplateServiceReference, GetTranslationTemplate25Channel); + return _translationTemplateServiceReference; #endif } @@ -1630,6 +1792,15 @@ public TranslationTemplate25ServiceReference.TranslationTemplate GetTranslationT /// /// The proxy public BackgroundTask25ServiceReference.BackgroundTask GetBackgroundTask25Channel() + { + return RetryOnFaultProxy.Wrap(EnsureBackgroundTask25Channel(), EnsureBackgroundTask25Channel); + } + /// + /// Ensures a live (non-faulted) channel, rebuilding it if necessary, and returns the raw (unwrapped) channel - + /// used both as the public accessor's initial target and as the rebuild delegate for retries in . + /// + /// The raw proxy, never wrapped. + private BackgroundTask25ServiceReference.BackgroundTask EnsureBackgroundTask25Channel() { var clientInnerChannelState = _backgroundTaskClient?.InnerChannel?.State; var serviceReferenceState = (_backgroundTaskServiceReference as ICommunicationObject)?.State; @@ -1650,7 +1821,7 @@ public BackgroundTask25ServiceReference.BackgroundTask GetBackgroundTask25Channe _backgroundTaskServiceReference = _backgroundTaskClient.ChannelFactory.CreateChannelWithIssuedToken(WrapJwt(GetAccessToken().Value)); ((ICommunicationObject)_backgroundTaskServiceReference).Faulted += new EventHandler(BackgroundTaskClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_backgroundTaskServiceReference, GetBackgroundTask25Channel); + return _backgroundTaskServiceReference; #else if ((_backgroundTaskClient == null) || (_backgroundTaskServiceReference == null) || (clientInnerChannelState == CommunicationState.Faulted) || (serviceReferenceState == CommunicationState.Faulted) || @@ -1682,7 +1853,7 @@ public BackgroundTask25ServiceReference.BackgroundTask GetBackgroundTask25Channe _backgroundTaskServiceReference = _backgroundTaskClient.ChannelFactory.CreateChannel(); ((ICommunicationObject)_backgroundTaskServiceReference).Faulted += new EventHandler(BackgroundTaskClientChannelFaulted); } - return RetryOnFaultProxy.Wrap(_backgroundTaskServiceReference, GetBackgroundTask25Channel); + return _backgroundTaskServiceReference; #endif } #endregion diff --git a/Source/ISHRemote/Trisoft.ISHRemote/Connection/RetryOnFaultProxy.cs b/Source/ISHRemote/Trisoft.ISHRemote/Connection/RetryOnFaultProxy.cs index a16addf0..66b87674 100644 --- a/Source/ISHRemote/Trisoft.ISHRemote/Connection/RetryOnFaultProxy.cs +++ b/Source/ISHRemote/Trisoft.ISHRemote/Connection/RetryOnFaultProxy.cs @@ -31,9 +31,20 @@ namespace Trisoft.ISHRemote.Connection /// state checks, Faulted event wiring and Dispose() bookkeeping in InfoShareWcfSoapWithOpenIdConnectConnection /// keep operating on the underlying raw channel field directly, this proxy is only applied at the point where the /// channel is handed out to callers. + /// + /// Each public Get*25Channel() method is split into itself (which wraps exactly once) and a private + /// Ensure*25Channel() helper (which does the state-check/rebuild and returns the raw, unwrapped channel). The + /// rebuild delegate passed to must be that private helper - see the warning on . /// /// The generated WCF service-contract interface, for example Baseline25ServiceReference.Baseline. - internal sealed class RetryOnFaultProxy : DispatchProxy where T : class + /// + /// Must be public, not internal: on .NET Framework, + /// generates the proxy type in a new dynamic assembly under strict Reflection.Emit visibility checks, so a + /// non-public TProxy fails to load with "Access is denied" (observed on Windows PowerShell 5.1/net48). + /// .NET 6.0+ relaxes this check, so the same code works there regardless of visibility - keep it public so both + /// runtimes behave the same. + /// + public class RetryOnFaultProxy : DispatchProxy where T : class { private T _target; private Func _rebuild; @@ -44,8 +55,12 @@ internal sealed class RetryOnFaultProxy : DispatchProxy where T : class /// . /// /// Current channel instance, as currently held/rebuilt by the owning connection. - /// Delegate that returns a (possibly rebuilt) channel of type ; typically the same - /// Get*25Channel() method this proxy is being returned from. + /// Delegate that returns a (possibly rebuilt) channel of type . This + /// MUST be the raw, unwrapped channel accessor (e.g. a private Ensure*25Channel() helper) - never the public + /// Get*25Channel() method that itself calls . Passing a rebuild delegate that returns + /// another wrapped proxy causes each retry to recurse into a brand-new with its + /// own full retry budget; on a persistent (non-transient) fault this chains unbounded and causes a stack overflow + /// instead of a single bounded retry. /// A proxy implementing that transparently retries once on fault. public static T Wrap(T target, Func rebuild) {