Verbesserungen: Thread-Sicherheit, Logging, WCF-Updates

Die Änderungen umfassen:
- Einführung von `ReaderWriterLockSlim` für Thread-Sicherheit.
- Optimierung von `IsOnline` mit atomaren Operationen.
- Verbesserte Reconnect-Logik und Fehlerbehandlung.
- Neue Methoden für Datenbankoperationen (`GetDatatableByName` etc.).
- Gemeinsame `ChannelFactory` zur Ressourcenschonung.
- Logging-Verbesserungen mit präziseren Meldungen und Symbolen.
- Aktualisierte WCF-Bindings für höhere Verbindungszahlen und kürzere Timeouts.
- Aktualisierte Service-Referenzen mit asynchronen Methoden.
- Bereinigung von veralteten Methoden und Eigenschaften.
- Assembly-Version auf `2.8.0.0` aktualisiert.
This commit is contained in:
Developer01
2026-10-13 12:06:50 +02:00
parent aad2f01490
commit 3eb6314ba8
9 changed files with 4244 additions and 659 deletions

View File

@@ -22,8 +22,45 @@ Public Class Client
' Runtime Variables
Private _ServerAddress As ServerAddressStruct
Private _ClientConfig As GlobalStateClientConfiguration
Private ReadOnly _CachedTablesLock As New ReaderWriterLockSlim()
Private _CachedTables As New List(Of String)
Private _IsOnline As Boolean
Public ReadOnly Property CachedTables As List(Of String)
Get
_CachedTablesLock.EnterReadLock()
Try
Return _CachedTables.ToList() ' Snapshot zurückgeben
Finally
_CachedTablesLock.ExitReadLock()
End Try
End Get
End Property
' Hilfsmethode – intern für alle Schreibzugriffe verwenden
Private Sub SetCachedTables(pTables As IEnumerable(Of String))
_CachedTablesLock.EnterWriteLock()
Try
_CachedTables = pTables.ToList()
Finally
_CachedTablesLock.ExitWriteLock()
End Try
End Sub
' Ersetzt: Private _IsOnline As Boolean
Private _IsOnline As Integer = 0 ' 0 = offline, 1 = online (Interlocked-kompatibel)
Public ReadOnly Property IsOnline As Boolean
Get
Return Interlocked.CompareExchange(_IsOnline, 0, 0) = 1
End Get
End Property
' Setter-Hilfsmethoden für alle Schreibstellen:
Private Sub SetOnline()
Interlocked.Exchange(_IsOnline, 1)
End Sub
Private Sub SetOffline()
Interlocked.Exchange(_IsOnline, 0)
End Sub
' Verhindert parallele Reconnect-Versuche (0 = frei, 1 = läuft)
Private _IsReconnecting As Integer = 0
Private _Channel As IEDMIServiceChannel
@@ -56,14 +93,14 @@ Public Class Client
Protected Overridable Sub Dispose(disposing As Boolean)
If _Disposed Then Return
If disposing Then
_IsOnline = False ' ← Reconnect-Guard deaktivieren
SetOffline()
UpdateTimer.Stop()
UpdateTimer.Dispose()
' Auf laufenden Reconnect warten
Dim oTimeout = DateTime.Now.AddSeconds(5)
While _IsReconnecting = 1 AndAlso DateTime.Now < oTimeout
Task.Delay(100).Wait()
Thread.Sleep(100)
End While
If _Channel IsNot Nothing Then
@@ -71,6 +108,8 @@ Public Class Client
_Channel = Nothing
End If
_CachedTablesLock.Dispose() ' ← NEU: Lock freigeben
Dim oRemoved As ClientStatus = Nothing
_ActiveClients.TryRemove(_ClientId, oRemoved)
Logger.Debug("Client [{0}] unregistered. Total active clients: [{1}]", _ClientName, _ActiveClients.Count)
@@ -79,11 +118,6 @@ Public Class Client
End Sub
' Public Variables
Public ReadOnly Property CachedTables
Get
Return _CachedTables
End Get
End Property
Public ReadOnly Property ClientConfig As GlobalStateClientConfiguration
Get
@@ -94,13 +128,6 @@ Public Class Client
Return _ClientConfig
End Get
End Property
Public ReadOnly Property IsOnline As Boolean
Get
Return _IsOnline
End Get
End Property
Public ReadOnly Property ServerAddress As String
Get
Return $"{_ServerAddress.Host}:{_ServerAddress.Port}"
@@ -197,14 +224,14 @@ Public Class Client
' CachedTables sofort befüllen – nicht auf den ersten Timer-Tick warten
Try
Dim oTables As String() = _Channel.GetCachedTables()
_CachedTables = oTables.Select(Function(t) t.ToUpper).ToList()
Logger.Debug("✔ CachedTables loaded on connect: [{0}]", _CachedTables.Count)
SetCachedTables(oTables.Select(Function(t) t.ToUpper()))
Logger.Debug("✔ CachedTables loaded on connect: [{0}]", CachedTables.Count)
Catch ex As Exception
Logger.Warn("⚠ CachedTables could not be loaded on connect: {0}", ex.Message)
_CachedTables = New List(Of String)
SetCachedTables(Enumerable.Empty(Of String)())
End Try
_IsOnline = True ' ← vor GetStatus()
SetOnline()
UpdateTimer.Start()
' In Registry eintragen – jetzt mit korrektem IsOnline=True
@@ -213,7 +240,7 @@ Public Class Client
Return True
Catch ex As Exception
_IsOnline = False
SetOffline()
Logger.Error(ex)
Return False
End Try
@@ -243,7 +270,7 @@ Public Class Client
_Channel = ChannelManager.GetChannel()
_Channel.Open()
_IsOnline = True
SetOnline()
If _ActiveClients.ContainsKey(_ClientId) Then
_ActiveClients(_ClientId) = GetStatus()
@@ -253,7 +280,7 @@ Public Class Client
Return
Catch ex As Exception
Logger.Warn("⚠ Reconnect failed after [{0}]s: {1}", oDelay, ex.Message)
_IsOnline = False
SetOffline()
End Try
Next
@@ -266,53 +293,51 @@ Public Class Client
Private Async Sub UpdateTimer_Elapsed(sender As Object, e As Timers.ElapsedEventArgs) Handles UpdateTimer.Elapsed
Try
If _Channel Is Nothing OrElse _Channel.State <> CommunicationState.Opened Then
Logger.Warn("⚠ UpdateTimer skipped: Channel not in Opened state [{0}]", If(_Channel IsNot Nothing, _Channel.State.ToString(), "null"))
_IsOnline = False
' ← Snapshot VOR dem Await – schützt gegen _Channel = Nothing nach Resume
Dim oChannel = _Channel
If oChannel Is Nothing OrElse oChannel.State <> CommunicationState.Opened Then
Logger.Warn("⚠ UpdateTimer skipped: Channel not in Opened state [{0}]",
If(oChannel IsNot Nothing, oChannel.State.ToString(), "null"))
SetOffline()
Reconnect(sender, e)
Return
End If
If Not _IsOnline Then
If Not IsOnline Then
Logger.Debug("UpdateTimer skipped: Client is offline.")
Return
End If
Dim oTables As String() = Await _Channel.GetCachedTablesAsync()
' ◄ NullReferenceException-Schutz: Service kann Nothing zurückgeben
Dim oTables As String() = Await oChannel.GetCachedTablesAsync() ' ← oChannel statt _Channel
If oTables Is Nothing Then
Logger.Warn("⚠ UpdateTimer: GetCachedTablesAsync returned Nothing.")
_CachedTables = New List(Of String)
SetCachedTables(Enumerable.Empty(Of String)())
Return
End If
_CachedTables = oTables.
Select(Function(table) table.ToUpper).
ToList()
SetCachedTables(oTables.Select(Function(table) table.ToUpper()))
' ← oChannel.State statt _Channel.State
Logger.Info("✔ Channel state: [{0}] | Active connections: [{1}] | CachedTables: [{2}]",
_Channel.State,
Channel.ActiveConnections,
_CachedTables.Count)
oChannel.State, Channel.ActiveConnections, CachedTables.Count)
If _ActiveClients.ContainsKey(_ClientId) Then
_ActiveClients(_ClientId) = GetStatus()
End If
Catch ex As TimeoutException
' Stale TCP connection (z.B. Firewall/Citrix Idle-Timeout)
Logger.Warn("⚠ UpdateTimer: TCP connection lost (idle timeout / firewall). Triggering reconnect...")
_IsOnline = False
Logger.Warn("⚠ UpdateTimer: TCP connection lost. Triggering reconnect...")
SetOffline()
Reconnect()
Catch ex As CommunicationException
Logger.Warn("⚠ UpdateTimer: Channel faulted. Triggering reconnect...")
_IsOnline = False
SetOffline()
Reconnect()
Catch ex As Exception
Logger.Warn("⚠ Update of CachedTable was not successful!")
Logger.Error(ex)
_CachedTables = New List(Of String)
SetCachedTables(Enumerable.Empty(Of String)())
End Try
End Sub
@@ -626,6 +651,59 @@ Public Class Client
End Function
#Region "GetDatatable"
Public Function GetDatatableByName(DatatableName As String, Optional FilterExpression As String = "", Optional SortByColumn As String = "") As TableResult
EnsureAvailable()
Try
Return _Channel.ReturnDatatableFromCache(DatatableName, FilterExpression, SortByColumn)
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetDatatableByName))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableByName))
Catch ex As CommunicationException
HandleCommunicationError(ex, NameOf(GetDatatableByName))
Catch ex As Exception
Logger.Error(ex)
Throw
End Try
End Function
Public Function GetScalarValueFromIDB(pSQL As String) As GetScalarValueResponse
EnsureAvailable()
Try
Return _Channel.ReturnScalarValue(New GetScalarValueRequest() With {
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.IDB
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetScalarValueFromIDB))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetScalarValueFromIDB))
Catch ex As CommunicationException
HandleCommunicationError(ex, NameOf(GetScalarValueFromIDB))
Catch ex As Exception
Logger.Error(ex)
Throw
End Try
End Function
Public Function ExecuteNonQueryFromIDB(pSQL As String) As ExecuteNonQueryResponse
EnsureAvailable()
Try
Return _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With {
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.IDB
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDB))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDB))
Catch ex As CommunicationException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDB))
Catch ex As Exception
Logger.Error(ex)
Throw
End Try
End Function
Public Function GetDatatableFromIDB(pSQL As String) As GetDatatableResponse
EnsureAvailable()
Try
@@ -633,6 +711,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.IDB
})
Catch ex As TimeoutException ' ← NEU
HandleCommunicationError(ex, NameOf(GetDatatableFromIDB))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableFromIDB))
Catch ex As CommunicationException
@@ -643,6 +723,7 @@ Public Class Client
End Try
End Function
' ── GetDatatable ──────────────────────────────────────────
Public Function GetDatatableFromECM(pSQL As String) As GetDatatableResponse
EnsureAvailable()
Try
@@ -650,6 +731,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.ECM
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetDatatableFromECM))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableFromECM))
Catch ex As CommunicationException
@@ -668,6 +751,8 @@ Public Class Client
.NamedDatabase = DatabaseName.None,
.ConnectionId = pConnectionId
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetDatatableFromConnection))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableFromConnection))
Catch ex As CommunicationException
@@ -685,6 +770,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.IDB
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetDatatableFromIDBAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableFromIDBAsync))
Catch ex As CommunicationException
@@ -703,6 +790,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.ECM
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetDatatableFromECMAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableFromECMAsync))
Catch ex As CommunicationException
@@ -721,6 +810,8 @@ Public Class Client
.NamedDatabase = DatabaseName.None,
.ConnectionId = pConnectionId
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetDatatableFromConnectionAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableFromConnectionAsync))
Catch ex As CommunicationException
@@ -730,26 +821,8 @@ Public Class Client
Throw
End Try
End Function
#End Region
#Region "GetScalarValue"
Public Function GetScalarValueFromIDB(pSQL As String) As GetScalarValueResponse
EnsureAvailable()
Try
Return _Channel.ReturnScalarValue(New GetScalarValueRequest() With {
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.IDB
})
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetScalarValueFromIDB))
Catch ex As CommunicationException
HandleCommunicationError(ex, NameOf(GetScalarValueFromIDB))
Catch ex As Exception
Logger.Error(ex)
Throw
End Try
End Function
' ── GetScalarValue ─────────────────────────────────────────
Public Function GetScalarValueFromECM(pSQL As String) As GetScalarValueResponse
EnsureAvailable()
Try
@@ -757,6 +830,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.ECM
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetScalarValueFromECM))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetScalarValueFromECM))
Catch ex As CommunicationException
@@ -775,6 +850,8 @@ Public Class Client
.NamedDatabase = DatabaseName.None,
.ConnectionId = pConnectionId
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetScalarValueFromConnection))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetScalarValueFromConnection))
Catch ex As CommunicationException
@@ -792,6 +869,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.IDB
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetScalarValueFromIDBAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetScalarValueFromIDBAsync))
Catch ex As CommunicationException
@@ -809,6 +888,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.ECM
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetScalarValueFromECMAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetScalarValueFromECMAsync))
Catch ex As CommunicationException
@@ -827,6 +908,8 @@ Public Class Client
.NamedDatabase = DatabaseName.None,
.ConnectionId = pConnectionId
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetScalarValueFromConnectionAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetScalarValueFromConnectionAsync))
Catch ex As CommunicationException
@@ -836,26 +919,8 @@ Public Class Client
Throw
End Try
End Function
#End Region
#Region "ExecuteNonQuery"
Public Function ExecuteNonQueryFromIDB(pSQL As String) As ExecuteNonQueryResponse
EnsureAvailable()
Try
Return _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With {
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.IDB
})
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDB))
Catch ex As CommunicationException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDB))
Catch ex As Exception
Logger.Error(ex)
Throw
End Try
End Function
' ── ExecuteNonQuery ────────────────────────────────────────
Public Function ExecuteNonQueryFromECM(pSQL As String) As ExecuteNonQueryResponse
EnsureAvailable()
Try
@@ -863,6 +928,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.ECM
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECM))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECM))
Catch ex As CommunicationException
@@ -881,6 +948,8 @@ Public Class Client
.NamedDatabase = DatabaseName.None,
.ConnectionId = pConnectionId
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnection))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnection))
Catch ex As CommunicationException
@@ -898,6 +967,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.IDB
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDBAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDBAsync))
Catch ex As CommunicationException
@@ -915,6 +986,8 @@ Public Class Client
.SqlCommand = pSQL,
.NamedDatabase = DatabaseName.ECM
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECMAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECMAsync))
Catch ex As CommunicationException
@@ -933,6 +1006,8 @@ Public Class Client
.NamedDatabase = DatabaseName.None,
.ConnectionId = pConnectionId
})
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnectionAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnectionAsync))
Catch ex As CommunicationException
@@ -942,45 +1017,26 @@ Public Class Client
Throw
End Try
End Function
' ── Sonstige ───────────────────────────────────────────────
Public Async Function GetDatatableByNameAsync(DatatableName As String, Optional FilterExpression As String = "", Optional SortByColumn As String = "") As Task(Of TableResult)
EnsureAvailable()
Try
Return Await _Channel.ReturnDatatableFromCacheAsync(DatatableName, FilterExpression, SortByColumn)
Catch ex As TimeoutException
HandleCommunicationError(ex, NameOf(GetDatatableByNameAsync))
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableByNameAsync))
Catch ex As CommunicationException
HandleCommunicationError(ex, NameOf(GetDatatableByNameAsync))
Catch ex As Exception
Logger.Error(ex)
Throw
End Try
End Function
#End Region
Public Function GetDatatableByName(DatatableName As String, Optional FilterExpression As String = "", Optional SortByColumn As String = "") As TableResult
EnsureAvailable()
Try
Return _Channel.ReturnDatatableFromCache(DatatableName, FilterExpression, SortByColumn)
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableByName))
Catch ex As CommunicationException
HandleCommunicationError(ex, NameOf(GetDatatableByName))
Catch ex As Exception
Logger.Error(ex)
Throw
End Try
End Function
Public Async Function GetDatatableByNameAsync(DatatableName As String, Optional FilterExpression As String = "", Optional SortByColumn As String = "") As Task(Of TableResult)
EnsureAvailable()
Try
Return Await _Channel.ReturnDatatableFromCacheAsync(DatatableName, FilterExpression, SortByColumn)
Catch ex As EndpointNotFoundException
HandleCommunicationError(ex, NameOf(GetDatatableByNameAsync))
Catch ex As CommunicationException
HandleCommunicationError(ex, NameOf(GetDatatableByNameAsync))
Catch ex As Exception
Logger.Error(ex)
Throw
End Try
End Function
''' <summary>
''' Return infos about a file object
''' </summary>
''' <param name="UserId"></param>
''' <param name="ObjectId"></param>
''' <returns></returns>
Public Function GetDocumentInfo(UserId As Long, ObjectId As Long) As DocumentInfo
Try
Dim oResponse As DocumentInfoResponse = _Channel.GetFileInfoByObjectId(New DocumentInfoRequest With {
@@ -1056,27 +1112,25 @@ Public Class Client
Throw New InvalidOperationException("Channel is not initialized. Call Connect() first.")
End If
If Not _IsOnline Then
' Kurze Grace-Period abwarten, damit Reconnect() seinen _IsReconnecting-Flag setzen kann
Dim oGraceLimit = DateTime.Now.AddMilliseconds(600)
While DateTime.Now < oGraceLimit AndAlso _IsReconnecting = 0 AndAlso Not _IsOnline
Task.Delay(100).Wait()
End While
If Not IsOnline Then
' Wenn Reconnect aktiv läuft: kurze Grace-Period (max 1s) statt 10s
If _IsReconnecting = 1 Then
Dim oGraceLimit = DateTime.Now.AddMilliseconds(1000)
While DateTime.Now < oGraceLimit AndAlso Not IsOnline
Thread.Sleep(100)
End While
End If
' Jetzt auf aktiven Reconnect warten
Dim oWaitLimit = DateTime.Now.AddSeconds(10)
While Not _IsOnline AndAlso DateTime.Now < oWaitLimit AndAlso _IsReconnecting = 1
Task.Delay(500).Wait()
Logger.Debug("EnsureAvailable: Waiting for reconnect... Channel=[{0}]", _Channel.State.ToString())
End While
If Not _IsOnline Then
Throw New InvalidOperationException("Client is offline.")
' Danach sofort werfen – WCF-Timeout nicht weiter aufbrauchen
If Not IsOnline Then
Throw New InvalidOperationException(
$"Client is offline. Reconnect in progress=[{_IsReconnecting = 1}].")
End If
End If
If _Channel.State <> CommunicationState.Opened Then
Throw New InvalidOperationException($"Channel is not available. State=[{_Channel.State}]")
Throw New InvalidOperationException(
$"Channel is not available. State=[{_Channel.State}]")
End If
End Sub
''' <summary>
@@ -1084,6 +1138,8 @@ Public Class Client
''' </summary>
Private Sub HandleCommunicationError(ex As Exception, callerName As String)
Select Case True
Case TypeOf ex Is TimeoutException
Logger.Warn("⚠ TimeoutException in [{0}]. Channel may be stale. Triggering reconnect...", callerName)
Case TypeOf ex Is EndpointNotFoundException
Logger.Warn("⚠ Service not reachable (EndpointNotFoundException in [{0}]). Triggering reconnect...", callerName)
Case TypeOf ex Is CommunicationException
@@ -1093,7 +1149,7 @@ Public Class Client
Logger.Error(ex)
End Select
_IsOnline = False
SetOffline()
Reconnect()
Throw ex
End Sub
@@ -1190,15 +1246,16 @@ Public Class Client
''' Returns a detailed status snapshot of the current client state
''' </summary>
Public Function GetStatus() As ClientStatus
Dim oTables = CachedTables ' ← Property nutzen: enthält Read-Lock + Snapshot
Return New ClientStatus() With {
.IsOnline = _IsOnline,
.ClientId = _ClientId, ' ← Neu
.IsOnline = IsOnline, ' ← Property statt Feld
.ClientId = _ClientId,
.ClientName = _ClientName,
.ServerAddress = Me.ServerAddress,
.ChannelState = If(_Channel IsNot Nothing, _Channel.State.ToString(), "null"),
.ActiveConnections = Channel.ActiveConnections,
.CachedTableCount = _CachedTables.Count,
.CachedTables = _CachedTables.ToList(),
.CachedTableCount = oTables.Count, ' ← aus Snapshot
.CachedTables = oTables, ' ← aus Snapshot
.HasClientConfig = _ClientConfig IsNot Nothing,
.IsDisposed = _Disposed,
.TimerEnabled = UpdateTimer.Enabled,

View File

@@ -8,10 +8,14 @@ Imports DigitalData.Modules.Logging
Public Class Channel
Inherits BaseClass
Private Shared ReadOnly _FactoryLock As New Object()
Private Shared _SharedFactory As ChannelFactory(Of IEDMIServiceChannel) = Nothing
Private Shared _SharedFactoryEndpoint As String = String.Empty
Private ReadOnly ChannelFactory As ChannelFactory(Of IEDMIServiceChannel)
Public Event Reconnect As EventHandler
' Verbindungszähler
Private Shared _ActiveConnections As Integer = 0
Public Shared ReadOnly Property ActiveConnections As Integer
Get
@@ -20,40 +24,91 @@ Public Class Channel
End Property
Public Shared Function GetBinding(Optional AuthenticationMode As TcpClientCredentialType = TcpClientCredentialType.Windows) As NetTcpBinding
Dim oMaxConnections = Math.Max(500, Environment.ProcessorCount * 50)
Return New NetTcpBinding() With {
.MaxReceivedMessageSize = Constants.ChannelSettings.MAX_RECEIVED_MESSAGE_SIZE,
.MaxBufferSize = Constants.ChannelSettings.MAX_BUFFER_SIZE,
.MaxBufferPoolSize = Constants.ChannelSettings.MAX_BUFFER_POOL_SIZE,
.ListenBacklog = Constants.ChannelSettings.MAX_CONNECTIONS,
.MaxConnections = Constants.ChannelSettings.MAX_CONNECTIONS,
.TransferMode = TransferMode.Streamed,
.SendTimeout = TimeSpan.FromSeconds(30), ' ← War: 5min → Schnellere Fehlererkennung
.ReceiveTimeout = TimeSpan.FromMinutes(10), ' Keepalive < Firewall-Timeout (3600s) ✓
.OpenTimeout = TimeSpan.FromSeconds(30), ' ← Neu: Verbindungsaufbau begrenzen
.CloseTimeout = TimeSpan.FromSeconds(30),
.Security = New NetTcpSecurity() With {
.Mode = SecurityMode.Transport,
.Transport = New TcpTransportSecurity() With {
.ClientCredentialType = AuthenticationMode
.MaxReceivedMessageSize = Constants.ChannelSettings.MAX_RECEIVED_MESSAGE_SIZE,
.MaxBufferSize = Constants.ChannelSettings.MAX_BUFFER_SIZE,
.MaxBufferPoolSize = Constants.ChannelSettings.MAX_BUFFER_POOL_SIZE,
.ListenBacklog = oMaxConnections,
.MaxConnections = oMaxConnections,
.TransferMode = TransferMode.Streamed,
.SendTimeout = TimeSpan.FromSeconds(60),
.ReceiveTimeout = TimeSpan.FromMinutes(10),
.OpenTimeout = TimeSpan.FromSeconds(60),
.CloseTimeout = TimeSpan.FromSeconds(30),
.Security = New NetTcpSecurity() With {
.Mode = SecurityMode.Transport,
.Transport = New TcpTransportSecurity() With {
.ClientCredentialType = AuthenticationMode
}
},
.ReaderQuotas = New XmlDictionaryReaderQuotas() With {
.MaxArrayLength = Constants.ChannelSettings.MAX_ARRAY_LENGTH,
.MaxStringContentLength = Constants.ChannelSettings.MAX_STRING_CONTENT_LENGTH
}
},
.ReaderQuotas = New XmlDictionaryReaderQuotas() With {
.MaxArrayLength = Constants.ChannelSettings.MAX_ARRAY_LENGTH,
.MaxStringContentLength = Constants.ChannelSettings.MAX_STRING_CONTENT_LENGTH
}
}
End Function
Public Sub New(pLogConfig As LogConfig, pServerAddress As ServerAddressStruct)
MyBase.New(pLogConfig)
ChannelFactory = GetChannelFactory(pServerAddress)
ChannelFactory = GetOrCreateSharedFactory(pLogConfig, pServerAddress)
End Sub
''' <summary>
''' Gibt eine gemeinsame ChannelFactory zurück.
''' Ein Connection-Pool für alle Client-Instanzen desselben Endpoints.
''' </summary>
Private Shared Function GetOrCreateSharedFactory(pLogConfig As LogConfig, pServerAddress As ServerAddressStruct) As ChannelFactory(Of IEDMIServiceChannel)
Dim oEndpoint = $"net.tcp://{pServerAddress.Host}:{pServerAddress.Port}/DigitalData/Services/Main"
' ← Volatile.Read: garantiert aktuellen Wert auch ohne Lock
Dim oCurrentFactory = Volatile.Read(_SharedFactory)
If oCurrentFactory IsNot Nothing AndAlso
_SharedFactoryEndpoint = oEndpoint AndAlso
(oCurrentFactory.State = CommunicationState.Opened OrElse
oCurrentFactory.State = CommunicationState.Created) Then
Return oCurrentFactory
End If
SyncLock _FactoryLock
' Double-check nach Lock
If _SharedFactory IsNot Nothing AndAlso
_SharedFactoryEndpoint = oEndpoint AndAlso
(_SharedFactory.State = CommunicationState.Opened OrElse
_SharedFactory.State = CommunicationState.Created) Then
Return _SharedFactory
End If
If _SharedFactory IsNot Nothing Then
If _SharedFactory.State = CommunicationState.Faulted OrElse
_SharedFactory.State = CommunicationState.Closing Then
Try
_SharedFactory.Abort()
Catch
End Try
End If
_SharedFactory = Nothing
End If
Dim oLogger = pLogConfig.GetLogger()
Dim oBinding = GetBinding()
Dim oAddress = New EndpointAddress(oEndpoint)
oLogger.Debug("Creating shared ChannelFactory | Endpoint=[{0}] | MaxConnections=[{1}] | SendTimeout=[{2}] | OpenTimeout=[{3}]",
oEndpoint, oBinding.MaxConnections, oBinding.SendTimeout, oBinding.OpenTimeout)
_SharedFactory = New ChannelFactory(Of IEDMIServiceChannel)(oBinding, oAddress)
_SharedFactoryEndpoint = oEndpoint
Return _SharedFactory
End SyncLock
End Function
''' <summary>
''' Closes a channel gracefully, falls back to Abort on error
''' </summary>
Public Shared Sub CloseChannel(pChannel As IEDMIServiceChannel)
If pChannel Is Nothing Then Return
Try
If pChannel.State = CommunicationState.Opened Then
pChannel.Close()
@@ -61,31 +116,27 @@ Public Class Channel
pChannel.State = CommunicationState.Closing Then
pChannel.Abort()
End If
' CommunicationState.Created / Closed → nichts tun, Zähler nicht betroffen
Catch ex As Exception
Try
pChannel.Abort()
Catch
' Ignore
End Try
End Try
End Sub
''' <summary>
''' Creates a channel and adds a Faulted-Handler
''' </summary>
''' <returns>A channel object</returns>
Public Function GetChannel() As IEDMIServiceChannel
Try
Logger.Debug("Creating channel.")
Logger.Debug("Creating channel from shared factory.")
Dim oChannel = ChannelFactory.CreateChannel()
' Verbindung zählen beim Öffnen
AddHandler oChannel.Opened, Sub()
Dim oCount = Interlocked.Increment(_ActiveConnections)
Logger.Debug("Channel opened. Active connections: [{0}]", oCount)
End Sub
' Verbindung abziehen beim Schließen oder Fehler
AddHandler oChannel.Closed, Sub()
Dim oCount = Interlocked.Decrement(_ActiveConnections)
Logger.Debug("Channel closed. Active connections: [{0}]", oCount)
@@ -93,7 +144,7 @@ Public Class Channel
AddHandler oChannel.Faulted, Sub()
Dim oCount = Interlocked.Decrement(_ActiveConnections)
Logger.Warn("Channel faulted. Active connections: [{0}]", oCount)
Logger.Warn("⚠ Channel faulted. Active connections: [{0}]", oCount)
RaiseEvent Reconnect(Me, Nothing)
End Sub
@@ -104,20 +155,4 @@ Public Class Channel
Throw
End Try
End Function
Private Function GetChannelFactory(pServerAddress As ServerAddressStruct) As ChannelFactory(Of IEDMIServiceChannel)
Dim oBinding = GetBinding()
Dim oAddress = New EndpointAddress($"net.tcp://{pServerAddress.Host}:{pServerAddress.Port}/DigitalData/Services/Main")
Logger.Debug("Creating ChannelFactory with Binding: SendTimeout=[{0}], ReceiveTimeout=[{1}], MaxConnections=[{2}], MaxBufferSize=[{3}], TransferMode=[{4}]",
oBinding.SendTimeout,
oBinding.ReceiveTimeout,
oBinding.MaxConnections,
oBinding.MaxBufferSize,
oBinding.TransferMode)
Dim oFactory = New ChannelFactory(Of IEDMIServiceChannel)(oBinding, oAddress)
Return oFactory
End Function
End Class
End Class

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<ReferenceGroup xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ID="1b516c6d-d2d0-4dfc-9557-3dd0593fa624" xmlns="urn:schemas-microsoft-com:xml-wcfservicemap">
<ReferenceGroup xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="1b516c6d-d2d0-4dfc-9557-3dd0593fa624" xmlns="urn:schemas-microsoft-com:xml-wcfservicemap">
<ClientOptions>
<GenerateAsynchronousMethods>false</GenerateAsynchronousMethods>
<GenerateTaskBasedAsynchronousMethod>true</GenerateTaskBasedAsynchronousMethod>

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<configurationSnapshot xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:schemas-microsoft-com:xml-wcfconfigurationsnapshot">
<configurationSnapshot xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:schemas-microsoft-com:xml-wcfconfigurationsnapshot">
<behaviors />
<bindings>
<binding digest="System.ServiceModel.Configuration.NetTcpBindingElement, System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089:&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-16&quot;?&gt;&lt;Data name=&quot;NetTcpBinding_IEDMIService&quot; transferMode=&quot;Streamed&quot;&gt;&lt;security&gt;&lt;transport sslProtocols=&quot;None&quot; /&gt;&lt;/security&gt;&lt;/Data&gt;" bindingType="netTcpBinding" name="NetTcpBinding_IEDMIService" />

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<SavedWcfConfigurationInformation xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Version="9.1" CheckSum="LUfq4Z7qL259JRuSf1xmDS/kLlBkZTtgk2vJyQH0Jvo=">
<SavedWcfConfigurationInformation xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Version="9.1" CheckSum="4uBt9Y5m0Ue4fIPG6eooT5TT5MJ97hiEcthL6+tUJHw=">
<bindingConfigurations>
<bindingConfiguration bindingType="netTcpBinding" name="NetTcpBinding_IEDMIService">
<properties>

View File

@@ -342,14 +342,17 @@ Public Class DatabaseWithFallback
End Function
Private Async Function GetDatatableFromServiceAsync(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Task(Of DataTable)
Dim oFallback As Boolean = False
_Logger.Debug("GetDatatableFromServiceAsync: GetDatatableFromServiceAsync called with SQL [{0}], DatabaseType [{1}], ConnectionId [{2}]", pSQLCommand, DatabaseType.ToString, pConnectionId)
_Logger.Debug("GetDatatableFromServiceAsync called | SQL=[{0}] | DB=[{1}] | ConnId=[{2}]",
pSQLCommand, DatabaseType.ToString, pConnectionId)
If _Client Is Nothing OrElse _ClientConfig?.ForceDirectDatabaseAccess Then
Return Await Task.Run(Function() GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId))
' ← Kein Task.Run: direkt synchron, Thread ist ohnehin frei
Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
End If
Dim oResult As GetDatatableResponse = Nothing
Try
Dim oResult As GetDatatableResponse = Nothing
Select Case DatabaseType
Case Constants.DatabaseType.ECM
oResult = Await _Client.GetDatatableFromECMAsync(pSQLCommand)
@@ -360,20 +363,22 @@ Public Class DatabaseWithFallback
End Select
If oResult Is Nothing OrElse oResult.OK = False Then
_Logger.Warn("⚠ GetDatatableFromServiceAsync failed. Falling back to direct database access.")
oFallback = True
_Logger.Warn("⚠ GetDatatableFromServiceAsync failed | Fallback to DB | Error=[{0}]",
If(oResult Is Nothing, "null", oResult.ErrorMessage))
Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
End If
Return oResult.Table
Catch ex As InvalidOperationException
_Logger.Warn("⚠ GetDatatableFromServiceAsync offline: [{0}] | Fallback to DB", ex.Message)
Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
Catch ex As Exception
_Logger.Warn("⚠ GetDatatableFromServiceAsync failed. Falling back to direct database access.")
_Logger.Warn("⚠ GetDatatableFromServiceAsync failed | Fallback to DB")
_Logger.Error(ex)
oFallback = True
Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
End Try
If oFallback Then
Return Await Task.Run(Function() GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId))
End If
Return oResult.Table
End Function
Private Function GetDatatableFromDatabase(pSQLCommand As String, pDatabaseType As Constants.DatabaseType, pConnectionId As Integer) As DataTable
Try
@@ -401,16 +406,16 @@ Public Class DatabaseWithFallback
End Try
End Function
Private Async Function GetScalarValueFromServiceAsync(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Task(Of Object)
Dim oFallback As Boolean = False
_Logger.Debug("GetScalarValueFromServiceAsync called with SQL [{0}], DatabaseType [{1}], ConnectionId [{2}]", pSQLCommand, DatabaseType.ToString, pConnectionId)
_Logger.Debug("GetScalarValueFromServiceAsync called | SQL=[{0}] | DB=[{1}] | ConnId=[{2}]",
pSQLCommand, DatabaseType.ToString, pConnectionId)
If _Client Is Nothing OrElse _ClientConfig?.ForceDirectDatabaseAccess Then
Return Await Task.Run(Function() GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId))
Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) ' ← kein Task.Run
End If
Dim oResult As GetScalarValueResponse = Nothing
Try
Dim oResult As GetScalarValueResponse = Nothing
Select Case DatabaseType
Case Constants.DatabaseType.ECM
oResult = Await _Client.GetScalarValueFromECMAsync(pSQLCommand)
@@ -421,20 +426,21 @@ Public Class DatabaseWithFallback
End Select
If oResult Is Nothing OrElse oResult.OK = False Then
_Logger.Warn("⚠ GetScalarValueFromServiceAsync failed. Falling back to direct database access.")
oFallback = True
_Logger.Warn("⚠ GetScalarValueFromServiceAsync failed | Fallback to DB")
Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
End If
Return oResult.Scalar
Catch ex As InvalidOperationException
_Logger.Warn("⚠ GetScalarValueFromServiceAsync offline: [{0}] | Fallback to DB", ex.Message)
Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
Catch ex As Exception
_Logger.Warn("⚠ GetScalarValueFromServiceAsync failed. Falling back to direct database access.")
_Logger.Warn("⚠ GetScalarValueFromServiceAsync failed | Fallback to DB")
_Logger.Error(ex)
oFallback = True
Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
End Try
If oFallback Then
Return Await Task.Run(Function() GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId))
End If
Return oResult.Scalar
End Function
Private Function GetScalarValueFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Object
Try
@@ -493,16 +499,16 @@ Public Class DatabaseWithFallback
End Try
End Function
Private Async Function ExecuteNonQueryFromServiceAsync(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Task(Of Boolean)
Dim oFallback As Boolean = False
_Logger.Debug("ExecuteNonQueryFromServiceAsync called with SQL [{0}], DatabaseType [{1}], ConnectionId [{2}]", pSQLCommand, DatabaseType.ToString, pConnectionId)
_Logger.Debug("ExecuteNonQueryFromServiceAsync called | SQL=[{0}] | DB=[{1}] | ConnId=[{2}]",
pSQLCommand, DatabaseType.ToString, pConnectionId)
If _Client Is Nothing OrElse _ClientConfig?.ForceDirectDatabaseAccess Then
Return Await Task.Run(Function() ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId))
Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) ' ← kein Task.Run
End If
Dim oResult As ExecuteNonQueryResponse = Nothing
Try
Dim oResult As ExecuteNonQueryResponse = Nothing
Select Case DatabaseType
Case Constants.DatabaseType.ECM
oResult = Await _Client.ExecuteNonQueryFromECMAsync(pSQLCommand)
@@ -513,20 +519,21 @@ Public Class DatabaseWithFallback
End Select
If oResult Is Nothing OrElse oResult.OK = False Then
_Logger.Warn("⚠ ExecuteNonQueryFromServiceAsync failed. Falling back to direct database access.")
oFallback = True
_Logger.Warn("⚠ ExecuteNonQueryFromServiceAsync failed | Fallback to DB")
Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
End If
Return oResult.Result
Catch ex As InvalidOperationException
_Logger.Warn("⚠ ExecuteNonQueryFromServiceAsync offline: [{0}] | Fallback to DB", ex.Message)
Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
Catch ex As Exception
_Logger.Warn("⚠ ExecuteNonQueryFromServiceAsync failed. Falling back to direct database access.")
_Logger.Warn("⚠ ExecuteNonQueryFromServiceAsync failed | Fallback to DB")
_Logger.Error(ex)
oFallback = True
Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId)
End Try
If oFallback Then
Return Await Task.Run(Function() ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId))
End If
Return oResult.Result
End Function
Private Function ExecuteNonQueryFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Boolean
Try

View File

@@ -13,7 +13,7 @@ Imports System.Runtime.InteropServices
<Assembly: AssemblyCompany("Digital Data")>
<Assembly: AssemblyProduct("EDMIAPI")>
<Assembly: AssemblyCopyright("Copyright © 2026")>
<Assembly: AssemblyTrademark("2.7.0")>
<Assembly: AssemblyTrademark("2.8.0")>
<Assembly: ComVisible(False)>
@@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices
' übernehmen, indem Sie "*" eingeben:
' <Assembly: AssemblyVersion("1.0.*")>
<Assembly: AssemblyVersion("2.7.0.0")>
<Assembly: AssemblyVersion("2.8.0.0")>
<Assembly: AssemblyFileVersion("1.6.1.1")>

3401
EDMIAPI/Reference.vb Normal file

File diff suppressed because it is too large Load Diff