diff --git a/Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/ManagedTransportChannelLease.cs b/Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/ManagedTransportChannelLease.cs
index ba5201c227..86d7731287 100644
--- a/Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/ManagedTransportChannelLease.cs
+++ b/Stack/Opc.Ua.Core/Stack/Client/Channels/Internal/ManagedTransportChannelLease.cs
@@ -275,10 +275,42 @@ namespace Opc.Ua
return DisposeAsyncCore();
}
- ///
+ ///
+ /// Synchronous dispose. Begins lease teardown asynchronously on
+ /// a thread-pool thread and returns immediately. Callers that
+ /// need to observe teardown completion or surface failures MUST
+ /// use . The
+ /// synchronous path exists only for compatibility with the
+ /// contract and the legacy
+ /// using statement, and never blocks on network I/O —
+ /// blocking would deadlock callers running under a synchronization
+ /// context (legacy ASP.NET, WPF, WinForms).
+ ///
public void Dispose()
{
- DisposeAsyncCore().AsTask().GetAwaiter().GetResult();
+ // Sync Dispose is best-effort. Mark the lease released
+ // immediately (preventing any further SendRequestAsync /
+ // ReconnectAsync calls), then push the actual network I/O
+ // onto the thread pool. This decouples the synchronous
+ // caller from the TCP FIN handshake — see
+ // ChannelEntry.TearDownAsync — and avoids deadlocking
+ // callers running under a synchronization context.
+ if (Interlocked.Exchange(ref m_active, 0) == 0)
+ {
+ return;
+ }
+ ChannelEntry entry = Entry;
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await entry.ReleaseLeaseAsync(this).ConfigureAwait(false);
+ }
+ catch
+ {
+ // best effort — sync Dispose cannot surface async failures
+ }
+ });
}
private async ValueTask DisposeAsyncCore()