Imports System.IO Imports System.ServiceModel Imports System.Threading Imports DigitalData.Modules.Base Imports DigitalData.Modules.EDMI.API.Constants Imports DigitalData.Modules.EDMI.API.EDMIServiceReference Imports DigitalData.Modules.EDMI.API.Rights Imports DigitalData.Modules.Logging Public Class Client Implements IDisposable ' Constants Private Const UPDATE_INTERVAL_IN_MINUTES As Integer = 1 Private Const KIND_TYPE_DOC = "DOC" ' Helper Classes Private ReadOnly LogConfig As LogConfig Private ReadOnly Logger As Logger Private ReadOnly ChannelManager As Channel ' Runtime Variables Private _ServerAddress As ServerAddressStruct Private _ClientConfig As GlobalStateClientConfiguration Private _CachedTables As New List(Of String) Private _IsOnline As Boolean ' Verhindert parallele Reconnect-Versuche (0 = frei, 1 = läuft) Private _IsReconnecting As Integer = 0 Private _Channel As IEDMIServiceChannel Private _ClientName As String ' Static Registry Private Shared ReadOnly _ActiveClients As New Collections.Concurrent.ConcurrentDictionary(Of String, ClientStatus) Private ReadOnly _ClientId As String ' ← Neu: eindeutiger interner Key Public Shared ReadOnly Property ActiveClients As IReadOnlyDictionary(Of String, ClientStatus) Get Return _ActiveClients End Get End Property ''' ''' Returns a snapshot of all currently registered client instances ''' Public Shared Function GetAllClientStatus() As List(Of ClientStatus) Return _ActiveClients.Values.ToList() End Function ' Update Timer Private WithEvents UpdateTimer As New Timers.Timer Private _Disposed As Boolean = False ''' ''' Closes the channel and releases all resources ''' Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub Protected Overridable Sub Dispose(disposing As Boolean) If _Disposed Then Return If disposing Then _IsOnline = False ' ← Reconnect-Guard deaktivieren 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() End While If _Channel IsNot Nothing Then Channel.CloseChannel(_Channel) _Channel = Nothing End If Dim oRemoved As ClientStatus = Nothing _ActiveClients.TryRemove(_ClientId, oRemoved) Logger.Debug("Client [{0}] unregistered. Total active clients: [{1}]", _ClientName, _ActiveClients.Count) End If _Disposed = True End Sub ' Public Variables Public ReadOnly Property CachedTables Get Return _CachedTables End Get End Property Public ReadOnly Property ClientConfig As GlobalStateClientConfiguration Get If _ClientConfig Is Nothing Then Throw New ApplicationException("ClientConfig is empty! Please connect to the service before accessing this property") End If 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}" End Get End Property ''' ''' Parse a IPAddress:Port String into its parts ''' ''' A Server Address, for example: 192.168.1.50, 192.168.1.50:9000, 192.168.1.50;9000 Public Shared Function ParseServiceAddress(AddressWithOptionalPort As String) As Tuple(Of String, Integer) Dim oConnection As New Connection() Dim oAddress As ServerAddressStruct = oConnection.ParseServiceAddress(AddressWithOptionalPort) Return New Tuple(Of String, Integer)(oAddress.Host, oAddress.Port) End Function ''' ''' Creates a new EDMI Client object ''' ''' LogConfig object ''' The IP address/hostname and port, separated by semicolon or colon, ex. 1.2.3.4:9000 ''' Optional identifier for this client instance (e.g. application name or hostname) Public Sub New(pLogConfig As LogConfig, pServiceAdress As String, Optional pClientName As String = "") LogConfig = pLogConfig Logger = pLogConfig.GetLogger() _ClientName = If(String.IsNullOrWhiteSpace(pClientName), Environment.MachineName, pClientName) _ClientId = $"{_ClientName}_{Guid.NewGuid().ToString("N").Substring(0, 8)}" ' ← eindeutig UpdateTimer.Interval = 60 * 1000 * UPDATE_INTERVAL_IN_MINUTES ' Start erfolgt in Connect() Try Dim oConnection = New Connection() _ServerAddress = oConnection.ParseServiceAddress(pServiceAdress) ChannelManager = New Channel(pLogConfig, _ServerAddress) AddHandler ChannelManager.Reconnect, AddressOf Reconnect Logger.Debug("Ready for connection to Service at: [{0}:{1}] | ClientName=[{2}] | ClientId=[{3}]", _ServerAddress.Host, _ServerAddress.Port, _ClientName, _ClientId) Catch ex As Exception Logger.Error(ex) End Try End Sub ''' ''' Creates a new EDMI Client object ''' ''' LogConfig object ''' The IP address to connect to ''' The Port number to use for the connection ''' Optional identifier for this client instance (e.g. application name or hostname) Public Sub New(pLogConfig As LogConfig, pIPAddress As String, pPortNumber As Integer, Optional pClientName As String = "") LogConfig = pLogConfig Logger = pLogConfig.GetLogger() _ClientName = If(String.IsNullOrWhiteSpace(pClientName), Environment.MachineName, pClientName) _ClientId = $"{_ClientName}_{Guid.NewGuid().ToString("N").Substring(0, 8)}" ' ← eindeutig UpdateTimer.Interval = 60 * 1000 * UPDATE_INTERVAL_IN_MINUTES Try Dim oConnection = New Connection() _ServerAddress = oConnection.ParseServiceAddress(pIPAddress, pPortNumber) ChannelManager = New Channel(pLogConfig, _ServerAddress) AddHandler ChannelManager.Reconnect, AddressOf Reconnect Logger.Debug("Ready for connection to Service at: [{0}:{1}] | ClientName=[{2}] | ClientId=[{3}]", _ServerAddress.Host, _ServerAddress.Port, _ClientName, _ClientId) Catch ex As Exception Logger.Error(ex) End Try End Sub ''' ''' Connect to the service ''' ''' True if connection was successful, false otherwise Public Function Connect() As Boolean Try _Channel = ChannelManager.GetChannel() Logger.Debug("Opening channel..") _Channel.Open() Dim oResponse = _Channel.GetClientConfig() If oResponse.OK Then _ClientConfig = oResponse.ClientConfig Else Logger.Warn("⚠Client Configuration could not be loaded: [{0}]", oResponse.ErrorMessage) End If Logger.Info("✔✔Connection to AppService [{0}] successfully established! | Client=[{1}] | Id=[{2}]", ServerAddress, _ClientName, _ClientId) ' 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) Catch ex As Exception Logger.Warn("⚠ CachedTables could not be loaded on connect: {0}", ex.Message) _CachedTables = New List(Of String) End Try _IsOnline = True ' ← vor GetStatus() UpdateTimer.Start() ' In Registry eintragen – jetzt mit korrektem IsOnline=True _ActiveClients(_ClientId) = GetStatus() Logger.Info("Client [{0}] registered with Id=[{1}]. Total active clients: [{2}]", _ClientName, _ClientId, _ActiveClients.Count) Return True Catch ex As Exception _IsOnline = False Logger.Error(ex) Return False End Try End Function ''' ''' Aborts the channel and creates a new connection ''' Public Async Sub Reconnect(Optional sender As Object = Nothing, Optional e As EventArgs = Nothing) ' Nur ein Reconnect gleichzeitig erlauben – atomar prüfen und setzen If Interlocked.CompareExchange(_IsReconnecting, 1, 0) = 1 Then Logger.Debug("Reconnect already in progress – skipping duplicate call.") Return End If Try Logger.Warn("⚠ Connection faulted. Trying to reconnect..") Dim oRetryDelays = {2, 5, 15, 30} For Each oDelay In oRetryDelays Try Await Task.Delay(TimeSpan.FromSeconds(oDelay)) Logger.Debug("Reconnect attempt after [{0}]s delay...", oDelay) Channel.CloseChannel(_Channel) _Channel = ChannelManager.GetChannel() _Channel.Open() _IsOnline = True If _ActiveClients.ContainsKey(_ClientId) Then _ActiveClients(_ClientId) = GetStatus() End If Logger.Info("✔ Reconnect successful.") Return Catch ex As Exception Logger.Warn("⚠ Reconnect failed after [{0}]s: {1}", oDelay, ex.Message) _IsOnline = False End Try Next Logger.Error("All reconnect attempts failed. Service unavailable.") Finally ' Flag immer zurücksetzen – auch bei unerwarteten Fehlern Interlocked.Exchange(_IsReconnecting, 0) End Try End Sub 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 Reconnect(sender, e) Return End If 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 If oTables Is Nothing Then Logger.Warn("⚠ UpdateTimer: GetCachedTablesAsync returned Nothing.") _CachedTables = New List(Of String) Return End If _CachedTables = oTables. Select(Function(table) table.ToUpper). ToList() Logger.Info("✔ Channel state: [{0}] | Active connections: [{1}] | CachedTables: [{2}]", _Channel.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 Reconnect() Catch ex As CommunicationException Logger.Warn("⚠ UpdateTimer: Channel faulted. Triggering reconnect...") _IsOnline = False Reconnect() Catch ex As Exception Logger.Warn("⚠ Update of CachedTable was not successful!") Logger.Error(ex) _CachedTables = New List(Of String) End Try End Sub ''' ''' Imports a file from a filepath, creating a IDB ObjectId and Filesystem Object ''' ''' Type of ObjectStore. Can be WORK or ARCHIVE. ''' Business entity that the new file object should belong to. ''' Other file import options ''' When local filepath was not found ''' When there was a error in the Service ''' The ObjectId of the newly generated filesystem object Public Async Function NewFileAsync(pFilePath As String, pObjectStoreName As String, pObjectKind As String, pIDBDoctypeId As Long, Optional pImportOptions As Options.NewFileOptions = Nothing) As Task(Of Long) Try Dim oNewFile As New Modules.IDB.NewFile(LogConfig, _Channel) Return Await oNewFile.RunAsync(pFilePath, pObjectStoreName, pObjectKind, pIDBDoctypeId, pImportOptions) Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Public Async Function UpdateFileAsync(pObjectId As Long, pFilePath As String, Optional pImportOptions As Options.UpdateFileOptions = Nothing) As Task(Of Long) Try Dim oUpdateFile As New Modules.IDB.UpdateFile(LogConfig, _Channel) Return Await oUpdateFile.RunAsync(pFilePath, pObjectId, pImportOptions) Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Public Async Function SetObjectStateAsync(pObjectId As Long, pState As String, Optional pOptions As Options.SetObjectStateOptions = Nothing) As Task(Of Boolean) Try Dim oSetObjectState As New Modules.IDB.SetObjectState(LogConfig, _Channel) Return Await oSetObjectState.RunAsync(pObjectId, pState, pOptions) Catch ex As Exception Logger.Error(ex) Return False End Try End Function Public Async Function SetAttributeValueAsync(pObjectId As Long, pName As String, pValue As Object, Optional pOptions As Options.SetAttributeValueOptions = Nothing) As Task(Of Boolean) Try Dim oSetAttributeValue As New Modules.IDB.SetAttributeValue(LogConfig, _Channel) Return Await oSetAttributeValue.RunAsync(pObjectId, pName, pValue, pOptions) Catch ex As Exception Logger.Error(ex) Return False End Try End Function Public Async Function CheckOutFile(pObjectId As Long, pComment As String, Optional pOptions As Options.CheckOutInOptions = Nothing) As Task(Of Long) Try Dim oCheckOutFile As New Modules.IDB.CheckOutFile(LogConfig, _Channel) Return Await oCheckOutFile.RunAsync(pObjectId, pComment, pOptions) Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Public Async Function CheckOutFile(pObjectId As Long, Optional pOptions As Options.CheckOutInOptions = Nothing) As Task(Of Long) Try Dim oCheckOutFile As New Modules.IDB.CheckOutFile(LogConfig, _Channel) Return Await oCheckOutFile.RunAsync(pObjectId, String.Empty, pOptions) Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Public Async Function CheckInFile(pObjectId As Long, Optional pOptions As Options.CheckOutInOptions = Nothing) As Task(Of Long) Try Dim oCheckInFile As New Modules.IDB.CheckInFile(LogConfig, _Channel) Return Await oCheckInFile.RunAsync(pObjectId, pOptions) Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Public Async Function ImportFileAsync( pFilePath As String, pAttributeValues As List(Of UserAttributeValue), pObjectStoreName As String, pObjectKind As String, pIDBDoctypeId As String, Optional pImportOptions As Options.ImportFileOptions = Nothing) As Task(Of ImportFileResponse) Try Dim oImportFile As New Modules.IDB.ImportFile(LogConfig, _Channel) Return Await oImportFile.RunAsync(pFilePath, pAttributeValues, pObjectStoreName, pObjectKind, pIDBDoctypeId, pImportOptions) Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Public Async Function Globix_ImportFileAsync( pFilePath As String, pProfileId As Integer, pAttributeValues As List(Of UserAttributeValue), pObjectStoreName As String, pObjectKind As String, pIDBDoctypeId As String, Optional pImportOptions As Options.ImportFileOptions = Nothing) As Task(Of Globix_ImportFileResponse) Try Dim oImportFile As New Modules.Globix.ImportFile(LogConfig, _Channel) Return Await oImportFile.RunAsync(pFilePath, pProfileId, pAttributeValues, pObjectStoreName, pObjectKind, pIDBDoctypeId, pImportOptions) Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Public Function Zooflow_GetFileObject(pObjectId As Long, pLoadFileContents As Boolean) As FileObject Try Dim oGetFileObject As New Modules.Zooflow.GetFileObject(LogConfig, _Channel) Dim oFileObject = oGetFileObject.Run(pObjectId, pLoadFileContents) Return oFileObject Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Public Async Function ZooFlow_GetFileObjectAsync(pObjectId As Long, pLoadFileContents As Boolean) As Task(Of FileObject) Try Dim oGetFileObject As New Modules.Zooflow.GetFileObject(LogConfig, Me) Dim oFileObject = Await oGetFileObject.RunAsync(pObjectId, pLoadFileContents) Return oFileObject Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function ''' ''' ''' ''' ''' ''' ''' Public Function GetVariableValue(pObjectId As Long, pAttributeName As String, Optional pOptions As Options.GetVariableValueOptions = Nothing) As VariableValue If pOptions Is Nothing Then pOptions = New Options.GetVariableValueOptions() End If Try Dim oArgs = New GetAttributeValueRequest With { .ObjectId = pObjectId, .AttributeName = pAttributeName, .User = New UserState() With { .UserName = pOptions.Username, .Language = pOptions.Language } } Dim oResponse = _Channel.GetAttributeValue(oArgs) If oResponse.OK = False Then Return New VariableValue() End If Return New VariableValue(oResponse.Value) Catch ex As Exception Logger.Error(ex) Return New VariableValue() End Try End Function Private Function GetValueByType(pRow As DataRow, pTypeString As String) As Object Try Dim oAttributeValue As Object Select Case pTypeString Case Constants.AttributeTypeName.BIT oAttributeValue = pRow.Item("ValueBigInt") Case Constants.AttributeTypeName.BIG_INTEGER oAttributeValue = pRow.Item("ValueBigInt") Case Constants.AttributeTypeName.DATE oAttributeValue = pRow.Item("ValueDate") Case Constants.AttributeTypeName.DATETIME oAttributeValue = pRow.Item("ValueDate") Case Constants.AttributeTypeName.DECIMAL oAttributeValue = pRow.Item("ValueDecimal") Case Constants.AttributeTypeName.FLOAT oAttributeValue = pRow.Item("ValueDecimal") Case Constants.AttributeTypeName.VARCHAR oAttributeValue = pRow.Item("ValueText") Case Constants.AttributeTypeName.VECTOR_INTEGER oAttributeValue = pRow.Item("ValueBigInt") Case Constants.AttributeTypeName.VECTOR_STRING oAttributeValue = pRow.Item("ValueText") Case Else oAttributeValue = Nothing End Select Return oAttributeValue Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Private Function GetAttributesForObject(pObjectId As Long, pLanguage As String) As List(Of ObjectAttribute) Dim oAttributes As New List(Of ObjectAttribute) Try Dim oResult As TableResult = _Channel.ReturnDatatable_MSSQL_IDB($"EXEC [PRIDB_GET_VALUE_DT] {pObjectId}, '{pLanguage}'") If oResult.OK = False Then Throw New ApplicationException(oResult.ErrorMessage) End If If oResult.Table Is Nothing OrElse oResult.Table.Rows.Count = 0 Then Return Nothing End If For Each oRow As DataRow In oResult.Table.Rows Dim oAttribute As New ObjectAttribute With { .Id = oRow.Item("AttributeId"), .Title = oRow.Item("AttributeTitle"), .Type = oRow.Item("AttributeType"), .ValueBigInt = oRow.ItemEx(Of Object)("ValueBigInt", Nothing), .ValueDate = oRow.ItemEx(Of Object)("ValueDate", Nothing), .ValueDecimal = oRow.ItemEx(Of Object)("ValueDecimal", Nothing), .ValueText = oRow.ItemEx(Of Object)("ValueText", Nothing) } oAttributes.Add(oAttribute) Next Return oAttributes Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function Private Function NewObjectData(pObjectId As Long, pAttributeName As String, pValue As Object, pOptions As Options.NewObjectOptions) If pOptions Is Nothing Then pOptions = New Options.NewObjectOptions() End If Dim oLanguage = GetUserLanguage(pOptions.Language) Dim oUsername = GetUserName(pOptions.Username) Dim oSql = $"DECLARE @NEW_OBJ_MD_ID BIGINT EXEC PRIDB_NEW_OBJ_DATA({pObjectId}, '{pAttributeName}', '{oUsername}', '{pValue}', '{oLanguage}', 0, @OMD_ID = @NEW_OBJ_MD_ID OUTPUT)" Dim oResult = _Channel.ExecuteNonQuery_MSSQL_IDB(oSql) If oResult.OK = False Then Logger.Warn("⚠ Error while IDBNewObjectData") Logger.Error(oResult.ErrorMessage) End If Return oResult.OK End Function Private Function DeleteTermObjectFromMetadata(pObjectId As Long, pAttributeName As String, pTerm2Delete As String, Optional pUsername As String = "", Optional pLanguage As String = "") As Boolean Try Dim oLanguage = GetUserLanguage(pLanguage) Dim oUsername = GetUserName(pUsername) Dim oIdIsForeign As Integer = 1 Dim oDELSQL = $"EXEC PRIDB_DELETE_TERM_OBJECT_METADATA {pObjectId},'{pAttributeName}','{pTerm2Delete}','{oUsername}','{oLanguage}',{oIdIsForeign}" Dim oResult = _Channel.ExecuteNonQuery_MSSQL_IDB(oDELSQL) If oResult.OK = False Then Logger.Warn("⚠ Error while deleting Term object") Logger.Error(oResult.ErrorMessage) End If Return oResult.OK Catch ex As Exception Logger.Error(ex) Return False End Try End Function #Region "GetDatatable" Public Function GetDatatableFromIDB(pSQL As String) As GetDatatableResponse EnsureAvailable() Try Return _Channel.ReturnDatatable(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetDatatableFromIDB)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetDatatableFromIDB)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Function GetDatatableFromECM(pSQL As String) As GetDatatableResponse EnsureAvailable() Try Return _Channel.ReturnDatatable(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetDatatableFromECM)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetDatatableFromECM)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Function GetDatatableFromConnection(pSQL As String, pConnectionId As Integer) As GetDatatableResponse EnsureAvailable() Try Return _Channel.ReturnDatatable(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetDatatableFromConnection)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetDatatableFromConnection)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function GetDatatableFromIDBAsync(pSQL As String) As Task(Of GetDatatableResponse) EnsureAvailable() Try Return Await _Channel.ReturnDatatableAsync(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetDatatableFromIDBAsync)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetDatatableFromIDBAsync)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function GetDatatableFromECMAsync(pSQL As String) As Task(Of GetDatatableResponse) EnsureAvailable() Try Logger.Debug("GetDatatableFromECMAsync will be called with SQL [{0}]", pSQL) Return Await _Channel.ReturnDatatableAsync(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetDatatableFromECMAsync)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetDatatableFromECMAsync)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function GetDatatableFromConnectionAsync(pSQL As String, Optional pConnectionId As Integer = 0) As Task(Of GetDatatableResponse) EnsureAvailable() Try Return Await _Channel.ReturnDatatableAsync(New GetDatatableRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetDatatableFromConnectionAsync)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetDatatableFromConnectionAsync)) Catch ex As Exception Logger.Error(ex) 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 Public Function GetScalarValueFromECM(pSQL As String) As GetScalarValueResponse EnsureAvailable() Try Return _Channel.ReturnScalarValue(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetScalarValueFromECM)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetScalarValueFromECM)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Function GetScalarValueFromConnection(pSQL As String, pConnectionId As Integer) As GetScalarValueResponse EnsureAvailable() Try Return _Channel.ReturnScalarValue(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetScalarValueFromConnection)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetScalarValueFromConnection)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function GetScalarValueFromIDBAsync(pSQL As String) As Task(Of GetScalarValueResponse) EnsureAvailable() Try Return Await _Channel.ReturnScalarValueAsync(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetScalarValueFromIDBAsync)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetScalarValueFromIDBAsync)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function GetScalarValueFromECMAsync(pSQL As String) As Task(Of GetScalarValueResponse) EnsureAvailable() Try Return Await _Channel.ReturnScalarValueAsync(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetScalarValueFromECMAsync)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetScalarValueFromECMAsync)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function GetScalarValueFromConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of GetScalarValueResponse) EnsureAvailable() Try Return Await _Channel.ReturnScalarValueAsync(New GetScalarValueRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(GetScalarValueFromConnectionAsync)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(GetScalarValueFromConnectionAsync)) Catch ex As Exception Logger.Error(ex) 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 Public Function ExecuteNonQueryFromECM(pSQL As String) As ExecuteNonQueryResponse EnsureAvailable() Try Return _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECM)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECM)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Function ExecuteNonQueryFromConnection(pSQL As String, pConnectionId As Integer) As ExecuteNonQueryResponse EnsureAvailable() Try Return _Channel.ExecuteNonQuery(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnection)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnection)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function ExecuteNonQueryFromIDBAsync(pSQL As String) As Task(Of ExecuteNonQueryResponse) EnsureAvailable() Try Return Await _Channel.ExecuteNonQueryAsync(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.IDB }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDBAsync)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromIDBAsync)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function ExecuteNonQueryFromECMAsync(pSQL As String) As Task(Of ExecuteNonQueryResponse) EnsureAvailable() Try Return Await _Channel.ExecuteNonQueryAsync(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.ECM }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECMAsync)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromECMAsync)) Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function ExecuteNonQueryFromConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of ExecuteNonQueryResponse) EnsureAvailable() Try Return Await _Channel.ExecuteNonQueryAsync(New ExecuteNonQueryRequest() With { .SqlCommand = pSQL, .NamedDatabase = DatabaseName.None, .ConnectionId = pConnectionId }) Catch ex As EndpointNotFoundException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnectionAsync)) Catch ex As CommunicationException HandleCommunicationError(ex, NameOf(ExecuteNonQueryFromConnectionAsync)) 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 ''' ''' Return infos about a file object ''' ''' ''' ''' Public Function GetDocumentInfo(UserId As Long, ObjectId As Long) As DocumentInfo Try Dim oResponse As DocumentInfoResponse = _Channel.GetFileInfoByObjectId(New DocumentInfoRequest With { .ObjectId = ObjectId, .UserId = UserId }) Return New DocumentInfo With { .AccessRight = oResponse.FileRight, .FullPath = oResponse.FullPath } Catch ex As FaultException(Of ObjectDoesNotExistFault) Logger.Error(ex) Throw Catch ex As FaultException Logger.Error(ex) Throw Catch ex As Exception Logger.Error(ex) Throw End Try End Function Public Async Function GetDocumentInfoAsync(UserId As Long, ObjectId As Long) As Task(Of DocumentInfo) Try Dim oParams = New DocumentInfoRequest With { .ObjectId = ObjectId, .UserId = UserId } Dim oResponse As DocumentInfoResponse = Await _Channel.GetFileInfoByObjectIdAsync(oParams) Return New DocumentInfo With { .AccessRight = oResponse.FileRight, .FullPath = oResponse.FullPath } Catch ex As Exception Logger.Error(ex) Throw End Try End Function ''' ''' Returns a live monitoring snapshot from the service (active sessions, users) ''' Public Function GetServiceStatus() As ServiceStatusResponse EnsureAvailable() Try Return _Channel.GetServiceStatus() Catch ex As Exception Logger.Error(ex) Return Nothing End Try End Function #Region "Private Functions" Private Function GetUserLanguage(pOverrideLanguage As String) As String Return ObjectEx.NotNull(pOverrideLanguage, Threading.Thread.CurrentThread.CurrentUICulture.Name) End Function Private Function GetUserName(pOverrideName) As String Return ObjectEx.NotNull(pOverrideName, Environment.UserName) End Function ''' ''' Throws InvalidOperationException if the channel is not ready for calls ''' Private Sub EnsureAvailable() If _Channel Is Nothing Then 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 ' 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.") End If End If If _Channel.State <> CommunicationState.Opened Then Throw New InvalidOperationException($"Channel is not available. State=[{_Channel.State}]") End If End Sub ''' ''' Logs the communication error, sets offline state, triggers reconnect and rethrows ''' Private Sub HandleCommunicationError(ex As Exception, callerName As String) Select Case True Case TypeOf ex Is EndpointNotFoundException Logger.Warn("⚠ Service not reachable (EndpointNotFoundException in [{0}]). Triggering reconnect...", callerName) Case TypeOf ex Is CommunicationException Logger.Warn("⚠ CommunicationException in [{0}] during call. Triggering reconnect...", callerName) Case Else Logger.Warn("⚠ Unexpected exception in [{0}] during call. Triggering reconnect...", callerName) Logger.Error(ex) End Select _IsOnline = False Reconnect() Throw ex End Sub #End Region #Region "Response Classes" Public Class StreamedFile Public Stream As MemoryStream Public FileName As String End Class Public Class FileList Public Datatable As DataTable End Class Public Class DocumentInfo Public Id As Long Public FullPath As String Public AccessRight As AccessRight End Class Public Class VariableValue Public ReadOnly Property IsVector As Boolean = False Public Property Value As Object Public Property Type As Type Public Sub New() MyClass.New(Nothing) End Sub Public Sub New(pValue As Object) ' Check if value is a collection If TypeOf pValue Is IEnumerable Then IsVector = True End If ' Try to determine the type If IsNothing(pValue) Then Type = Nothing Else Type = pValue.GetType End If Value = pValue End Sub End Class #End Region Public Class ObjectAttribute Public Property Id As Long Public Property Title As String Public Property Type As String Public Property ValueBigInt As Long Public Property ValueText As String Public Property ValueDecimal As Decimal Public Property ValueDate As DateTime Public ReadOnly Property Value As Object Get Return GetValue() End Get End Property Private Function GetValue() As Object Select Case Type Case AttributeTypeName.VECTOR_INTEGER Return ValueBigInt Case AttributeTypeName.BIG_INTEGER Return ValueBigInt Case AttributeTypeName.VECTOR_STRING Return ValueText Case AttributeTypeName.VARCHAR Return ValueText Case AttributeTypeName.BIT Return IIf(ValueBigInt = 1, True, False) Case AttributeTypeName.DATE Return ValueDate Case AttributeTypeName.DATETIME Return ValueDate Case AttributeTypeName.DECIMAL Return ValueDecimal Case AttributeTypeName.FLOAT Return ValueDecimal Case Else Return Nothing End Select End Function End Class ' ← ObjectAttribute endet hier ''' ''' Returns a detailed status snapshot of the current client state ''' Public Function GetStatus() As ClientStatus Return New ClientStatus() With { .IsOnline = _IsOnline, .ClientId = _ClientId, ' ← Neu .ClientName = _ClientName, .ServerAddress = Me.ServerAddress, .ChannelState = If(_Channel IsNot Nothing, _Channel.State.ToString(), "null"), .ActiveConnections = Channel.ActiveConnections, .CachedTableCount = _CachedTables.Count, .CachedTables = _CachedTables.ToList(), .HasClientConfig = _ClientConfig IsNot Nothing, .IsDisposed = _Disposed, .TimerEnabled = UpdateTimer.Enabled, .TimerIntervalSeconds = CInt(UpdateTimer.Interval / 1000), .Timestamp = DateTime.Now } End Function Public Class ClientStatus Public Property IsOnline As Boolean Public Property ClientId As String Public Property ClientName As String Public Property ServerAddress As String Public Property ChannelState As String Public Property ActiveConnections As Integer Public Property CachedTableCount As Integer Public Property CachedTables As List(Of String) Public Property HasClientConfig As Boolean Public Property IsDisposed As Boolean Public Property TimerEnabled As Boolean Public Property TimerIntervalSeconds As Integer Public Property Timestamp As DateTime Public Overrides Function ToString() As String Return String.Format( "[ClientStatus] ClientName=[{0}] Id=[{1}] Online=[{2}] Channel=[{3}]", ClientName, ClientId, IsOnline, ChannelState) End Function Public Function Client_Who_ToString() As String Return String.Format( "[ClientStatus] Client=[{0}] Id=[{1}] Online=[{2}] Server=[{3}] Channel=[{4}]", ClientName, ClientId, IsOnline, ServerAddress, ChannelState) End Function End Class ' ← ClientStatus endet hier – kein GetServiceStatus mehr drin End Class