Imports System.Collections.Concurrent Imports System.Data.SqlClient Imports DigitalData.Modules.Database Imports DigitalData.Modules.EDMI.API Imports DigitalData.Modules.EDMI.API.Constants Imports DigitalData.Modules.EDMI.API.EDMIServiceReference Imports DigitalData.Modules.Logging Public Class DatabaseWithFallback Private ReadOnly _Logger As Logger Private ReadOnly _Client As Client Private ReadOnly _ClientConfig As GlobalStateClientConfiguration Private ReadOnly _DatabaseECM As MSSQLServer Private _DatabaseIDB As MSSQLServer ''' ''' Lokaler Override-Cache für ExecuteAppServiceWrite-Änderungen. ''' Schlüssel: "TABELLENNAME|FILTERAUSDRUCK" → geänderter DataTable-Snapshot. ''' Wird beim nächsten Server-Refresh (Client.UpdateTimer) automatisch obsolet. ''' Private ReadOnly _localOverrideCache As New ConcurrentDictionary(Of String, DataTable)() ''' Erstellt den Cache-Key aus Tabellenname und Filter. Private Shared Function GetOverrideCacheKey(pTable As String, pFilter As String) As String Return $"{pTable.ToUpper}|{pFilter}" End Function ''' Leert den lokalen Override-Cache (z.B. nach manuellem Refresh). Public Sub InvalidateLocalOverrideCache() _localOverrideCache.Clear() _Logger.Debug("InvalidateLocalOverrideCache: Lokaler Override-Cache geleert.") End Sub ''' Leert den Override-Cache für eine bestimmte Tabelle. Public Sub InvalidateLocalOverrideCache(pTableName As String) Dim oKeysToRemove = _localOverrideCache.Keys. Where(Function(k) k.StartsWith(pTableName.ToUpper & "|")).ToList() For Each oKey In oKeysToRemove Dim oRemoved As DataTable = Nothing _localOverrideCache.TryRemove(oKey, oRemoved) Next _Logger.Debug("InvalidateLocalOverrideCache: Override-Cache für Tabelle [{0}] geleert.", pTableName) End Sub ''' ''' Thread-sicherer Cache für Connection Strings, indexiert nach Connection-ID. ''' Verhindert wiederholte DB-Roundtrips für SELECT * FROM TBDD_CONNECTION. ''' Private ReadOnly _connectionStringCache As New ConcurrentDictionary(Of Integer, String) ''' ''' Liefert den Connection String für eine bekannte Connection-ID. ''' Beim ersten Aufruf wird der Wert aus der DB geladen und gecacht. ''' Hinweis: Bei parallelen Erstabrufen derselben ID kann die Factory mehrfach ''' aufgerufen werden – da der Wert idempotent ist, ist dies funktional korrekt. ''' Private Function GetCachedConnectionString(pConnectionId As Integer) As String Return _connectionStringCache.GetOrAdd(pConnectionId, Function(id) _Logger.Debug("GetCachedConnectionString: Cache miss für ConnectionId [{0}] - lade aus DB", id) Dim oConnectionString = _DatabaseECM.Get_ConnectionStringforID(id) _Logger.Debug("GetCachedConnectionString: ConnectionId [{0}] gecacht", id) Return oConnectionString End Function) End Function ''' ''' Leert den Connection-String-Cache, z.B. nach Konfigurationsänderungen. ''' Public Sub InvalidateConnectionCache() _connectionStringCache.Clear() _Logger.Debug("InvalidateConnectionCache: Connection-String-Cache geleert") End Sub ''' ''' Leert den Cache für eine bestimmte Connection-ID. ''' Public Sub InvalidateConnectionCache(pConnectionId As Integer) Dim removed As String = Nothing If _connectionStringCache.TryRemove(pConnectionId, removed) Then _Logger.Debug("InvalidateConnectionCache: ConnectionId [{0}] aus Cache entfernt", pConnectionId) End If End Sub ''' ''' Options for GetDatatable ''' Public Class GetDatatableOptions Public ReadOnly FallbackSQL Public ReadOnly FallbackType ''' ''' Filter expression for the cached Datatable ''' Public Property FilterExpression As String = "" ''' ''' Columns to sort the cached Datatable by ''' Public Property SortByColumn As String = "" ''' ''' Force the fallback, skipping the service completely ''' Public Property ForceFallback As Boolean = False ''' ''' Connection Id to use, references TBDD_CONNECTION ''' Public Property ConnectionId As Integer = 0 ''' ''' Erkennt URI-Cache-Syntax: "TABELLENNAME?FILTERAUSDRUCK" ''' Beispiel: "TBDD_USER?USER_ID=4524" ''' Public ReadOnly Property IsCacheQuery As Boolean Get If String.IsNullOrWhiteSpace(FallbackSQL) Then Return False Dim oTrimmed As String = FallbackSQL.TrimStart() ' Nur wenn kein SQL-Schlüsselwort vorhanden → Cache-Routing-Syntax Return FallbackSQL.Contains("?") AndAlso Not oTrimmed.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("UPDATE", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.Contains("DECLARE") AndAlso Not oTrimmed.StartsWith("EXEC", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("DECLARE", StringComparison.OrdinalIgnoreCase) End Get End Property Public ReadOnly Property IsCacheWriteQuery As Boolean Get If String.IsNullOrWhiteSpace(FallbackSQL) Then Return False Dim oTrimmed As String = FallbackSQL.TrimStart() Return FallbackSQL.Contains("?") AndAlso FallbackSQL.Contains("!") AndAlso Not oTrimmed.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("UPDATE", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("EXEC", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("DECLARE", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("--", StringComparison.OrdinalIgnoreCase) AndAlso Not oTrimmed.StartsWith("/*", StringComparison.OrdinalIgnoreCase) End Get End Property ''' "TBDD_USER?USER_ID=4524" → "TBDD_USER" Public ReadOnly Property CacheQueryTable As String Get If Not IsCacheQuery Then Return String.Empty Return FallbackSQL.Substring(0, FallbackSQL.IndexOf("?")).Trim() End Get End Property ''' "TBDD_USER?USER_ID=4524" → "USER_ID=4524" Public ReadOnly Property CacheQueryFilter As String Get If Not IsCacheQuery Then Return String.Empty Return FallbackSQL.Substring(FallbackSQL.IndexOf("?") + 1).Trim() End Get End Property ''' "TBDD_USER?USER_ID=4524!STATUS=1" → "TBDD_USER" Public ReadOnly Property CacheWriteTable As String Get If Not IsCacheWriteQuery Then Return String.Empty Return FallbackSQL.Substring(0, FallbackSQL.IndexOf("?")).Trim() End Get End Property ''' "TBDD_USER?USER_ID=4524!STATUS=1" → "USER_ID=4524" Public ReadOnly Property CacheWriteFilter As String Get If Not IsCacheWriteQuery Then Return String.Empty Dim oStart As Integer = FallbackSQL.IndexOf("?") + 1 Dim oLength As Integer = FallbackSQL.IndexOf("!") - oStart Return FallbackSQL.Substring(oStart, oLength).Trim() End Get End Property ''' "TBDD_USER?USER_ID=4524!STATUS=1" → "STATUS=1" | "DELETE" Public ReadOnly Property CacheWriteAction As String Get If Not IsCacheWriteQuery Then Return String.Empty Return FallbackSQL.Substring(FallbackSQL.IndexOf("!") + 1).Trim() End Get End Property ''' True wenn die Aktion ein DELETE ist Public ReadOnly Property CacheWriteIsDelete As Boolean Get Return IsCacheWriteQuery AndAlso CacheWriteAction.Equals("DELETE", StringComparison.OrdinalIgnoreCase) End Get End Property ''' ''' Generiert das SQL aus der URI-Write-Syntax. ''' UPDATE: "TBDD_USER?USER_ID=4524!STATUS=1" → "UPDATE TBDD_USER SET STATUS=1 WHERE USER_ID=4524" ''' DELETE: "TBDD_USER?USER_ID=4524!DELETE" → "DELETE FROM TBDD_USER WHERE USER_ID=4524" ''' Public ReadOnly Property CacheWriteToSql As String Get If Not IsCacheWriteQuery Then Return String.Empty If CacheWriteIsDelete Then Return $"DELETE FROM {CacheWriteTable} WHERE {CacheWriteFilter}" Else Return $"UPDATE {CacheWriteTable} SET {CacheWriteAction} WHERE {CacheWriteFilter}" End If End Get End Property ''' ''' Creates a new options object for GetDatatable options ''' ''' SQL Command to execute as fallback ''' Named Database to use for the fallback SQL Command Public Sub New(pFallbackSQL As String, pFallbackType As Constants.DatabaseType) FallbackSQL = pFallbackSQL FallbackType = pFallbackType End Sub ''' ''' Creates a new options object using URI-Syntax (Cache-Query or URI-Write-Query). ''' FallbackType defaults to ECM – only relevant if URI detection fails and falls back to SQL. ''' Beispiel Read: "TBDD_USER?USER_ID=4524" ''' Beispiel Write: "TBDD_USER?USER_ID=4524!STATUS=1" ''' Beispiel Write: "TBDD_USER?USER_ID=4524!DELETE" ''' ''' URI-Syntax string Public Sub New(pURISyntax As String) FallbackSQL = pURISyntax FallbackType = Constants.DatabaseType.ECM End Sub End Class Public Sub New(LogConfig As LogConfig, Client As Client, DatabaseECM As MSSQLServer, DatabaseIDB As MSSQLServer) _Logger = LogConfig.GetLogger() _Client = Client _DatabaseECM = DatabaseECM _DatabaseIDB = DatabaseIDB _Logger.Debug("Client exists: [{0}]", Not IsNothing(Client)) _Logger.Debug("DatabaseECM exists: [{0}]", Not IsNothing(DatabaseECM)) _Logger.Debug("DatabaseIDB exists: [{0}]", Not IsNothing(DatabaseIDB)) ' Load client config, will throw if client is not yet connected to service _ClientConfig = Client?.ClientConfig End Sub ''' ''' Set the IDB Database class after initializing the class. ''' It is now your responsibility to make sure to not use any IDB calls before calling this method. ''' Public Sub InitializeIDB(pDatabaseIDB As MSSQLServer) _DatabaseIDB = pDatabaseIDB End Sub Public Function GetConnectionString(pConnectionId As Integer) As String Return _DatabaseECM.GetConnectionStringForId(pConnectionId) End Function #Region "GetDatatable" ''' URI-Cache-Syntax wird über GetDatatable(pOptions) ausgewertet. Public Function GetDatatableECM(pSQL As String) As DataTable Return GetDatatable(New GetDatatableOptions(pSQL, Constants.DatabaseType.ECM)) End Function Public Async Function GetDatatableECMAsync(pSQL As String) As Task(Of DataTable) Return Await GetDatatableFromServiceAsync(pSQL, Constants.DatabaseType.ECM, 0) End Function ''' URI-Cache-Syntax wird über GetDatatable(pOptions) ausgewertet. Public Function GetDatatableIDB(pSQL As String) As DataTable Return GetDatatable(New GetDatatableOptions(pSQL, Constants.DatabaseType.IDB)) End Function Public Async Function GetDatatableIDBAsync(pSQL As String) As Task(Of DataTable) Return Await GetDatatableFromServiceAsync(pSQL, Constants.DatabaseType.IDB, 0) End Function Public Function GetDatatableWithConnection(pSQL As String, pConnectionId As Integer) As DataTable ' ===== URI-CACHE-SYNTAX: "TABELLENNAME?FILTERAUSDRUCK" ===== Dim oOptions As New GetDatatableOptions(pSQL, Constants.DatabaseType.None) With {.ConnectionId = pConnectionId} If oOptions.IsCacheQuery Then Return GetDatatable(oOptions.CacheQueryTable, oOptions) End If Return GetDatatable(oOptions) End Function Public Async Function GetDatatableWithConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of DataTable) Return Await GetDatatableFromServiceAsync(pSQL, Constants.DatabaseType.None, pConnectionId) End Function #End Region #Region "GetScalarValue" Public Function GetScalarValueECM(pSQL As String) As Object Return GetScalarValue(pSQL, Constants.DatabaseType.ECM) End Function Public Async Function GetScalarValueECMAsync(pSQL As String) As Task(Of Object) Return Await GetScalarValueFromServiceAsync(pSQL, Constants.DatabaseType.ECM, 0) End Function Public Function GetScalarValueIDB(pSQL As String) As Object Return GetScalarValue(pSQL, Constants.DatabaseType.IDB) End Function Public Async Function GetScalarValueIDBAsync(pSQL As String) As Task(Of Object) Return Await GetScalarValueFromServiceAsync(pSQL, Constants.DatabaseType.IDB, 0) End Function Public Function GetScalarValueWithConnection(pSQL As String, pConnectionId As Integer) As Object Return GetScalarValue(pSQL, Constants.DatabaseType.None, pConnectionId:=pConnectionId) End Function Public Async Function GetScalarValueWithConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of Object) Return Await GetScalarValueFromServiceAsync(pSQL, Constants.DatabaseType.None, pConnectionId) End Function #End Region #Region "ExecuteNonQuery" ' URI-Cache-Syntax ("TABELLE?FILTER") ist hier nicht anwendbar: ' ExecuteNonQuery führt schreibende Operationen aus (INSERT/UPDATE/DELETE) ' und liefert kein Ergebnis, das aus dem AppServer-Cache gelesen werden könnte. Public Function ExecuteNonQueryECM(pSQL As String) As Boolean Return ExecuteNonQuery(pSQL, Constants.DatabaseType.ECM) End Function Public Async Function ExecuteNonQueryECMAsync(pSQL As String) As Task(Of Boolean) Return Await ExecuteNonQueryFromServiceAsync(pSQL, Constants.DatabaseType.ECM, 0) End Function Public Function ExecuteNonQueryIDB(pSQL As String) As Boolean Return ExecuteNonQuery(pSQL, Constants.DatabaseType.IDB) End Function Public Async Function ExecuteNonQueryIDBAsync(pSQL As String) As Task(Of Boolean) Return Await ExecuteNonQueryFromServiceAsync(pSQL, Constants.DatabaseType.IDB, 0) End Function Public Function ExecuteNonQueryWithConnection(pSQL As String, pConnectionId As Integer) As Boolean Return ExecuteNonQuery(pSQL, Constants.DatabaseType.None, pConnectionId:=pConnectionId) End Function Public Async Function ExecuteNonQueryWithConnectionAsync(pSQL As String, pConnectionId As Integer) As Task(Of Boolean) Return Await ExecuteNonQueryFromServiceAsync(pSQL, Constants.DatabaseType.None, pConnectionId) End Function #End Region ''' ''' Returns a Datatable by trying to fetch a cached version from the service, then querying the database through the service and querying the database directly as fallback. ''' ''' Name of the Cached Datatable ''' Options object Public Function GetDatatable(pDataTableName As String, pOptions As GetDatatableOptions) As DataTable ' ===== URI-CACHE-SYNTAX: "TABELLENNAME?FILTERAUSDRUCK" ===== If pOptions.IsCacheQuery AndAlso Not pOptions.ForceFallback AndAlso Not (_ClientConfig IsNot Nothing AndAlso _ClientConfig.ForceDirectDatabaseAccess) Then Dim oTable As String = pOptions.CacheQueryTable Dim oFilter As String = pOptions.CacheQueryFilter ' ===== LOKALER OVERRIDE-CACHE – vorrangig prüfen ===== Dim oCacheKey As String = GetOverrideCacheKey(oTable, oFilter) Dim oOverride As DataTable = Nothing If _localOverrideCache.TryGetValue(oCacheKey, oOverride) Then _Logger.Debug("✔ GetDatatable: Lokaler Override-Cache trifft für [{0}] Filter [{1}]", oTable, oFilter) Return oOverride End If _Logger.Debug("GetDatatable: URI-Cache-Query erkannt – Tabelle [{0}] Filter [{1}]", oTable, oFilter) If _Client IsNot Nothing AndAlso IsTableCached(oTable) Then Try Dim oResult As TableResult = _Client.GetDatatableByName(oTable, oFilter, pOptions.SortByColumn) If oResult IsNot Nothing AndAlso oResult.OK Then _Logger.Debug("✔ GetDatatable: URI-Cache-Query [{0}] – {1} Zeilen", oTable, oResult.Table?.Rows.Count) Return oResult.Table End If Catch ex As Exception _Logger.Error(ex) End Try _Logger.Warn("⚠ GetDatatable: URI-Cache-Query [{0}] fehlgeschlagen – kein SQL-Fallback verfügbar", oTable) Return Nothing End If _Logger.Warn("⚠ GetDatatable: URI-Cache-Query – Tabelle [{0}] nicht im AppServer-Cache", oTable) Return Nothing End If Return GetDatatable(pDataTableName, pOptions.FallbackSQL, pOptions.FallbackType, pOptions.FilterExpression, pOptions.SortByColumn, pOptions.ForceFallback, pOptions.ConnectionId) End Function Public Function GetDatatable(pSQL As String, pConnectionId As Integer) As DataTable ' ===== URI-CACHE-SYNTAX: "TABELLENNAME?FILTERAUSDRUCK" ===== Dim oOptions As New GetDatatableOptions(pSQL, Constants.DatabaseType.ECM) With {.ConnectionId = pConnectionId} If oOptions.IsCacheQuery Then Return GetDatatable(oOptions.CacheQueryTable, oOptions) End If Return GetDatatable("FORCE_FALLBACK", pSQL, Constants.DatabaseType.ECM, pSortByColumn:=Nothing, pForceFallback:=False, pConnectionId:=pConnectionId) End Function ''' ''' Returns a datatable directly from the database – or from AppServer cache if URI-Cache-Syntax is detected. ''' ''' Options object Public Function GetDatatable(pOptions As GetDatatableOptions) As DataTable ' ===== URI-CACHE-SYNTAX: "TABELLENNAME?FILTERAUSDRUCK" ===== If pOptions.IsCacheQuery Then Return GetDatatable(pOptions.CacheQueryTable, pOptions) End If Dim oForceFallback = True Return GetDatatable("FORCE_FALLBACK", pOptions.FallbackSQL, pOptions.FallbackType, pOptions.FilterExpression, pOptions.SortByColumn, oForceFallback, pOptions.ConnectionId) End Function ''' ''' Returns a Datatable by trying to fetch a cached version from the service, then querying the database through the service and querying the database directly as fallback. ''' ''' Name of the Cached Datatable ''' SQL Command to execute as fallback ''' Named Database to use for the fallback SQL Command ''' Filter expression for the cached Datatable ''' Columns to sort the cached Datatable by ''' Force the fallback, skipping the service completely ''' Connection Id to use, references TBDD_CONNECTION Public Function GetDatatable(pDataTableName As String, pFallbackSQL As String, pFallbackType As Constants.DatabaseType, Optional pFilterExpression As String = "", Optional pSortByColumn As String = "", Optional pForceFallback As Boolean = False, Optional pConnectionId As Integer = 0) As DataTable Try Dim oTableResult As TableResult = Nothing _Logger.Debug("GetDatatable: Fetching datatable [{0}] , Filter Expression [{1}], Sort By Column [{2}], Force Fallback [{3}]", pDataTableName, pFilterExpression, pSortByColumn, pForceFallback) ' If there is no client, we assume there is no service (configured) If _Client Is Nothing Then _Logger.Debug("GetDatatable: Client is empty, falling back to direct database access.") Return GetDatatableFromDatabase(pFallbackSQL, pFallbackType, pConnectionId) End If ' If ForceFallback flag is set, we go to database immediately If pForceFallback Or _ClientConfig.ForceDirectDatabaseAccess Then _Logger.Debug("GetDatatable: ForceFallback is True, falling back to direct database access.") Return GetDatatableFromDatabase(pFallbackSQL, pFallbackType, pConnectionId) End If ' If the table is not cached, we try going through the service If Not IsTableCached(pDataTableName) Then _Logger.Debug("GetDatatable: Datatable [{0}] is not chached, fetching data from service.", pDataTableName) Return GetDatatableFromService(pFallbackSQL, pFallbackType, pConnectionId) End If ' If there is a proper ConnectionId, we try going through the service If pConnectionId > 0 Then _Logger.Debug("GetDatatable: ConnectionId is set, fetching data from service.") Return GetDatatableFromService(pFallbackSQL, pFallbackType, pConnectionId) End If Try _Logger.Debug("GetDatatable: Datatable is chached, fetching data from cache.") oTableResult = _Client.GetDatatableByName(pDataTableName, pFilterExpression, pSortByColumn) Catch ex As Exception _Logger.Error(ex) oTableResult = Nothing End Try If oTableResult Is Nothing OrElse oTableResult.OK = False Then _Logger.Warn("⚠ GetDatatable: Datatable [{0}] could not be fetched from AppServer Cache. Falling back to direct Database Access.", pDataTableName) Return GetDatatableFromDatabase(pFallbackSQL, pFallbackType, pConnectionId) Else _Logger.Debug("✔ GetDatatable: Datatable [{0}] fetched from AppServer Cache.", pDataTableName) Return oTableResult.Table End If Catch ex As Exception _Logger.Error(ex) Return Nothing End Try End Function ''' ''' Returns a Scalar Value by querying the database through the service and querying the database directly as fallback. ''' URI-Cache-Syntax wird unterstützt: "TABELLENNAME?FILTERAUSDRUCK" – liefert Wert der ersten Spalte der ersten Zeile aus dem AppServer-Cache. ''' ''' SQL Command to execute as fallback – oder URI-Cache-Syntax "TABELLENNAME?FILTERAUSDRUCK" ''' Named Database to use for the fallback SQL Command ''' Force the fallback, skipping the service completely ''' Connection Id to use, references TBDD_CONNECTION Public Function GetScalarValue(pSQL As String, pDatabaseType As Constants.DatabaseType, Optional pForceFallback As Boolean = False, Optional pConnectionId As Integer = 0) As Object Try ' ===== URI-CACHE-SYNTAX: "TABELLENNAME?FILTERAUSDRUCK" ===== ' ForceFallback übersteuert auch die URI-Cache-Syntax Dim oOptions As New GetDatatableOptions(pSQL, pDatabaseType) If oOptions.IsCacheQuery AndAlso Not pForceFallback AndAlso Not (_ClientConfig IsNot Nothing AndAlso _ClientConfig.ForceDirectDatabaseAccess) Then Dim oTable As String = oOptions.CacheQueryTable Dim oFilter As String = oOptions.CacheQueryFilter _Logger.Debug("GetScalarValue: URI-Cache-Query erkannt – Tabelle [{0}] Filter [{1}]", oTable, oFilter) If _Client IsNot Nothing AndAlso IsTableCached(oTable) Then Try Dim oResult As TableResult = _Client.GetDatatableByName(oTable, oFilter, "") If oResult IsNot Nothing AndAlso oResult.OK AndAlso oResult.Table IsNot Nothing AndAlso oResult.Table.Rows.Count > 0 Then Dim oScalar As Object = oResult.Table.Rows(0)(0) _Logger.Debug("✔ GetScalarValue: URI-Cache-Query [{0}] – Scalar [{1}]", oTable, oScalar) Return oScalar End If Catch ex As Exception _Logger.Error(ex) End Try _Logger.Warn("⚠ GetScalarValue: URI-Cache-Query [{0}] fehlgeschlagen – kein SQL-Fallback verfügbar", oTable) Return Nothing End If _Logger.Warn("⚠ GetScalarValue: URI-Cache-Query – Tabelle [{0}] nicht im AppServer-Cache", oTable) Return Nothing End If If _Client Is Nothing Then Return GetScalarValueFromDatabase(pSQL, pDatabaseType, pConnectionId) End If If pForceFallback Or _ClientConfig.ForceDirectDatabaseAccess Then Return GetScalarValueFromDatabase(pSQL, pDatabaseType, pConnectionId) End If Return GetScalarValueFromService(pSQL, pDatabaseType, pConnectionId) Catch ex As Exception _Logger.Error(ex) Return Nothing End Try End Function ''' ''' Executes a non-query SQL command through the service and falls back to direct database access. ''' ''' SQL Command to execute ''' Named Database to use for the SQL Command ''' Force the fallback, skipping the service completely ''' Connection Id to use, references TBDD_CONNECTION Public Function ExecuteNonQuery(pSQL As String, pDatabaseType As Constants.DatabaseType, Optional pForceFallback As Boolean = False, Optional pConnectionId As Integer = 0) As Boolean Try ' If there is no client, we assume there is no service (configured) If _Client Is Nothing Then _Logger.Debug("ExecuteNonQuery - Client is empty, falling back to direct database access.") Return ExecuteNonQueryFromDatabase(pSQL, pDatabaseType, pConnectionId) End If ' If ForceFallback flag is set, we go to database immediately If pForceFallback Or _ClientConfig.ForceDirectDatabaseAccess Then _Logger.Debug("ExecuteNonQuery - ForceFallback is set, falling back to direct database access.") Return ExecuteNonQueryFromDatabase(pSQL, pDatabaseType, pConnectionId) End If Return ExecuteNonQueryFromService(pSQL, pDatabaseType, pConnectionId) Catch ex As Exception _Logger.Error(ex) Return False End Try End Function ''' ''' Führt ein UPDATE oder DELETE über URI-Write-Syntax aus. ''' Der geänderte Snapshot wird lokal gecacht und bei GetDatatable/GetScalarValue ''' vorrangig ausgewertet – bis der Server-Refresh den Cache neu befüllt. ''' UPDATE: "TBDD_USER?USER_ID=4524!STATUS=1" ''' DELETE: "TBDD_USER?USER_ID=4524!DELETE" ''' Public Function ExecuteAppServiceWrite(pOptions As GetDatatableOptions) As Boolean If Not pOptions.IsCacheWriteQuery Then _Logger.Warn("⚠ ExecuteAppServiceWrite: Keine gültige URI-Write-Syntax – [{0}] – Abbruch.", pOptions.FallbackSQL) Return False End If If _Client Is Nothing OrElse Not IsTableCached(pOptions.CacheWriteTable) Then _Logger.Warn("⚠ ExecuteAppServiceWrite: Tabelle [{0}] nicht im AppServer-Cache – Abbruch.", pOptions.CacheWriteTable) Return False End If _Logger.Debug("ExecuteAppServiceWrite: Tabelle [{0}] | Filter [{1}] | Aktion [{2}] | IsDelete [{3}]", pOptions.CacheWriteTable, pOptions.CacheWriteFilter, pOptions.CacheWriteAction, pOptions.CacheWriteIsDelete) Try ' Aktuellen Snapshot aus Cache laden Dim oResult As TableResult = _Client.GetDatatableByName(pOptions.CacheWriteTable, pOptions.CacheWriteFilter, "") If oResult Is Nothing OrElse Not oResult.OK OrElse oResult.Table Is Nothing Then _Logger.Warn("⚠ ExecuteAppServiceWrite: Cache-Abfrage für [{0}] fehlgeschlagen.", pOptions.CacheWriteTable) Return False End If Dim oMatchingRows As DataRow() = oResult.Table.Select(pOptions.CacheWriteFilter) If oMatchingRows.Length = 0 Then _Logger.Debug("ExecuteAppServiceWrite: Keine Zeilen für Filter [{0}] gefunden.", pOptions.CacheWriteFilter) Return True End If If pOptions.CacheWriteIsDelete Then For Each oRow As DataRow In oMatchingRows oRow.Delete() Next oResult.Table.AcceptChanges() _Logger.Debug("✔ ExecuteAppServiceWrite: DELETE – [{0}] Zeilen entfernt.", oMatchingRows.Length) Else For Each oPair As String In pOptions.CacheWriteAction.Split(","c) Dim oParts = oPair.Split("="c) If oParts.Length <> 2 Then Continue For Dim oCol As String = oParts(0).Trim() Dim oVal As String = oParts(1).Trim().Trim("'"c) For Each oRow As DataRow In oMatchingRows If oResult.Table.Columns.Contains(oCol) Then oRow(oCol) = oVal End If Next Next oResult.Table.AcceptChanges() _Logger.Debug("✔ ExecuteAppServiceWrite: UPDATE – [{0}] Zeilen aktualisiert.", oMatchingRows.Length) End If ' ===== OVERRIDE-CACHE BEFÜLLEN ===== Dim oCacheKey As String = GetOverrideCacheKey(pOptions.CacheWriteTable, pOptions.CacheWriteFilter) _localOverrideCache(oCacheKey) = oResult.Table _Logger.Debug("✔ ExecuteAppServiceWrite: Override-Cache gesetzt für [{0}]", oCacheKey) Return True Catch ex As Exception _Logger.Error(ex) Return False End Try End Function Private Function IsTableCached(pName As String) As Boolean _Logger.Debug("IsTableCached: Checking if table [{0}] is cached.", pName) If _Client Is Nothing Then _Logger.Warn("⚠ IsTableCached: Client is nothing, cannot check if table is cached.") Return False End If Return _Client.CachedTables.Contains(pName.ToUpper) End Function Private Function GetDatatableFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As DataTable Try _Logger.Debug("GetDatatableFromService: Fetching data from service [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) Dim oResult As GetDatatableResponse = Nothing Select Case DatabaseType Case Constants.DatabaseType.ECM oResult = _Client.GetDatatableFromECM(pSQLCommand) Case Constants.DatabaseType.IDB oResult = _Client.GetDatatableFromIDB(pSQLCommand) Case Else Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Select If oResult Is Nothing OrElse oResult.OK = False Then _Logger.Warn("⚠ GetDatatableFromService: Service returned invalid response [{0}]. Falling back to direct database access.", If(oResult Is Nothing, "null", oResult.ErrorMessage)) Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End If Return oResult.Table Catch ex As InvalidOperationException _Logger.Warn("⚠ GetDatatableFromService skipped: [{0}] Falling back to direct database access.", ex.Message) Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) Catch ex As Exception _Logger.Warn("⚠ GetDatatableFromService failed. Falling back to direct database access.") _Logger.Error(ex) Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Try End Function Private Async Function GetDatatableFromServiceAsync(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Task(Of DataTable) _Logger.Debug("GetDatatableFromServiceAsync called | SQL=[{0}] | DB=[{1}] | ConnId=[{2}]", pSQLCommand, DatabaseType.ToString, pConnectionId) If _Client Is Nothing OrElse _ClientConfig?.ForceDirectDatabaseAccess Then ' ← Kein Task.Run: direkt synchron, Thread ist ohnehin frei Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End If Try Dim oResult As GetDatatableResponse = Nothing Select Case DatabaseType Case Constants.DatabaseType.ECM oResult = Await _Client.GetDatatableFromECMAsync(pSQLCommand) Case Constants.DatabaseType.IDB oResult = Await _Client.GetDatatableFromIDBAsync(pSQLCommand) Case Else oResult = Await _Client.GetDatatableFromConnectionAsync(pSQLCommand, pConnectionId) End Select If oResult Is Nothing OrElse oResult.OK = False Then _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 | Fallback to DB") _Logger.Error(ex) Return GetDatatableFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Try End Function Private Function GetDatatableFromDatabase(pSQLCommand As String, pDatabaseType As Constants.DatabaseType, pConnectionId As Integer) As DataTable Try _Logger.Debug("GetDatatableFromDatabase: Fetching data from database [{0}] with Connection Id [{1}] and Command [{2}]", pDatabaseType.ToString, pConnectionId, pSQLCommand) Select Case pDatabaseType Case Constants.DatabaseType.IDB Return _DatabaseIDB.GetDatatable(pSQLCommand) Case Else If pConnectionId > 0 Then _Logger.Debug("Retrieving Connection String from Connection Id [{0}]", pConnectionId) Dim oConnectionString = GetCachedConnectionString(pConnectionId) ' ← NEU: Cache statt direkter DB-Abfrage Return _DatabaseECM.GetDatatableWithConnection(pSQLCommand, oConnectionString) Else Return _DatabaseECM.GetDatatable(pSQLCommand) End If End Select Catch ex As Exception _Logger.Error(ex) Return Nothing End Try End Function Private Async Function GetScalarValueFromServiceAsync(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Task(Of Object) _Logger.Debug("GetScalarValueFromServiceAsync called | SQL=[{0}] | DB=[{1}] | ConnId=[{2}]", pSQLCommand, DatabaseType.ToString, pConnectionId) If _Client Is Nothing OrElse _ClientConfig?.ForceDirectDatabaseAccess Then Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) ' ← kein Task.Run End If Try Dim oResult As GetScalarValueResponse = Nothing Select Case DatabaseType Case Constants.DatabaseType.ECM oResult = Await _Client.GetScalarValueFromECMAsync(pSQLCommand) Case Constants.DatabaseType.IDB oResult = Await _Client.GetScalarValueFromIDBAsync(pSQLCommand) Case Else oResult = Await _Client.GetScalarValueFromConnectionAsync(pSQLCommand, pConnectionId) End Select If oResult Is Nothing OrElse oResult.OK = False Then _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 | Fallback to DB") _Logger.Error(ex) Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Try End Function Private Function GetScalarValueFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Object Try Dim oResult As GetScalarValueResponse = Nothing _Logger.Debug("GetScalarValueFromService [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) Select Case DatabaseType Case Constants.DatabaseType.ECM oResult = _Client.GetScalarValueFromECM(pSQLCommand) Case Constants.DatabaseType.IDB oResult = _Client.GetScalarValueFromIDB(pSQLCommand) Case Else Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Select If oResult Is Nothing OrElse oResult.OK = False Then _Logger.Warn("⚠ GetScalarValueFromService: Service returned invalid response [{0}]. Falling back to direct database access.", If(oResult Is Nothing, "null", oResult.ErrorMessage)) Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End If Return oResult.Scalar Catch ex As InvalidOperationException _Logger.Warn("⚠ GetScalarValueFromService skipped: [{0}] Falling back to direct database access.", ex.Message) Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) Catch ex As Exception _Logger.Warn("⚠ GetScalarValueFromService failed. Falling back to direct database access.") _Logger.Error(ex) Return GetScalarValueFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Try End Function Private Function GetScalarValueFromDatabase(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Object Try _Logger.Debug("DBwFF GetScalarValueFromDatabase [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) Select Case DatabaseType Case Constants.DatabaseType.IDB Return _DatabaseIDB.GetScalarValue(pSQLCommand) Case Else If pConnectionId > 0 Then Dim oConnectionString = GetCachedConnectionString(pConnectionId) ' ← NEU: Cache statt direkter DB-Abfrage Return _DatabaseECM.GetScalarValueWithConnection(pSQLCommand, oConnectionString) Else Return _DatabaseECM.GetScalarValue(pSQLCommand) End If End Select Catch ex As Exception _Logger.Error(ex) Return Nothing End Try End Function Private Async Function ExecuteNonQueryFromServiceAsync(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Task(Of Boolean) _Logger.Debug("ExecuteNonQueryFromServiceAsync called | SQL=[{0}] | DB=[{1}] | ConnId=[{2}]", pSQLCommand, DatabaseType.ToString, pConnectionId) If _Client Is Nothing OrElse _ClientConfig?.ForceDirectDatabaseAccess Then Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) ' ← kein Task.Run End If Try Dim oResult As ExecuteNonQueryResponse = Nothing Select Case DatabaseType Case Constants.DatabaseType.ECM oResult = Await _Client.ExecuteNonQueryFromECMAsync(pSQLCommand) Case Constants.DatabaseType.IDB oResult = Await _Client.ExecuteNonQueryFromIDBAsync(pSQLCommand) Case Else oResult = Await _Client.ExecuteNonQueryFromConnectionAsync(pSQLCommand, pConnectionId) End Select If oResult Is Nothing OrElse oResult.OK = False Then _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 | Fallback to DB") _Logger.Error(ex) Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Try End Function Private Function ExecuteNonQueryFromService(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Boolean Try Dim oResult As ExecuteNonQueryResponse = Nothing _Logger.Debug("ExecuteNonQueryFromService [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) Select Case DatabaseType Case Constants.DatabaseType.ECM oResult = _Client.ExecuteNonQueryFromECM(pSQLCommand) Case Constants.DatabaseType.IDB oResult = _Client.ExecuteNonQueryFromIDB(pSQLCommand) Case Else Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Select If oResult Is Nothing OrElse oResult.OK = False Then _Logger.Warn("⚠ ExecuteNonQueryFromService: Service returned invalid response [{0}]. Falling back to direct database access.", If(oResult Is Nothing, "null", oResult.ErrorMessage)) Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End If Return oResult.Result Catch ex As InvalidOperationException _Logger.Warn("⚠ ExecuteNonQueryFromService skipped: [{0}] Falling back to direct database access.", ex.Message) Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) Catch ex As Exception _Logger.Warn("⚠ ExecuteNonQueryFromService failed. Falling back to direct database access.") _Logger.Error(ex) Return ExecuteNonQueryFromDatabase(pSQLCommand, DatabaseType, pConnectionId) End Try End Function Private Function ExecuteNonQueryFromDatabase(pSQLCommand As String, DatabaseType As Constants.DatabaseType, pConnectionId As Integer) As Boolean Try _Logger.Debug("ExecuteNonQueryFromDatabase [{0}] with Connection Id [{1}] and Command [{2}]", DatabaseType.ToString, pConnectionId, pSQLCommand) Select Case DatabaseType Case Constants.DatabaseType.IDB Return _DatabaseIDB.ExecuteNonQuery(pSQLCommand) Case Else If pConnectionId > 0 Then Dim oConnectionString = GetCachedConnectionString(pConnectionId) ' ← NEU: Cache statt direkter DB-Abfrage Return _DatabaseECM.ExecuteNonQueryWithConnection(pSQLCommand, oConnectionString) Else Return _DatabaseECM.ExecuteNonQuery(pSQLCommand) End If End Select Catch ex As Exception _Logger.Error(ex) Return False End Try End Function End Class