From ad2c6dbd53f6bd5a7f316d2d5a097c340575cec3 Mon Sep 17 00:00:00 2001 From: metacube Date: Fri, 17 Apr 2026 14:43:15 +0200 Subject: [PATCH] Refactor HANA access to async and parameterized queries --- .../DataSources/HanaDataSourceAdapter.cs | 4 +- .../Services/HanaQueryService.cs | 167 +++++++++++------- .../Services/IHanaQueryService.cs | 8 +- .../Services/SettingsPageService.cs | 2 +- .../Services/StandortePageService.cs | 14 +- 5 files changed, 118 insertions(+), 77 deletions(-) diff --git a/TrafagSalesExporter/Services/DataSources/HanaDataSourceAdapter.cs b/TrafagSalesExporter/Services/DataSources/HanaDataSourceAdapter.cs index 298ef1b..2d2b99f 100644 --- a/TrafagSalesExporter/Services/DataSources/HanaDataSourceAdapter.cs +++ b/TrafagSalesExporter/Services/DataSources/HanaDataSourceAdapter.cs @@ -35,8 +35,8 @@ public sealed class HanaDataSourceAdapter : IDataSourceAdapter siteId: site.Id, land: site.Land, details: exportServer.GetConnectionStringPreview()); - var records = await Task.Run(() => _hanaService.GetSalesRecords( - exportServer, site.Schema, site.TSC, site.Land, context.Settings.DateFilter)); + var records = await _hanaService.GetSalesRecordsAsync( + exportServer, site.Schema, site.TSC, site.Land, context.Settings.DateFilter); return new DataSourceFetchResult { Records = records }; } diff --git a/TrafagSalesExporter/Services/HanaQueryService.cs b/TrafagSalesExporter/Services/HanaQueryService.cs index 0944b61..b95a301 100644 --- a/TrafagSalesExporter/Services/HanaQueryService.cs +++ b/TrafagSalesExporter/Services/HanaQueryService.cs @@ -5,6 +5,8 @@ namespace TrafagSalesExporter.Services; public class HanaQueryService : IHanaQueryService { + private const string TscParameterName = "tsc"; + private const string DateFilterParameterName = "dateFilter"; private readonly IAppEventLogService _appEventLogService; public HanaQueryService(IAppEventLogService appEventLogService) @@ -12,39 +14,42 @@ public class HanaQueryService : IHanaQueryService _appEventLogService = appEventLogService; } - public List GetSalesRecords(HanaServer server, - string schema, string tsc, string land, string dateFilter) + public async Task> GetSalesRecordsAsync(HanaServer server, + string schema, string tsc, string land, string dateFilter, CancellationToken cancellationToken = default) { var connectionString = server.BuildConnectionString(); var result = new List(); try { - _appEventLogService.WriteAsync("HANA", "Verbindungsaufbau gestartet", land: land, - details: $"Server={server.GetConnectionStringPreview()} | Schema={schema} | TSC={tsc}").GetAwaiter().GetResult(); + await _appEventLogService.WriteAsync("HANA", "Verbindungsaufbau gestartet", land: land, + details: $"Server={server.GetConnectionStringPreview()} | Schema={schema} | TSC={tsc}"); using var connection = new HanaConnection(connectionString); - connection.Open(); + await connection.OpenAsync(cancellationToken); - _appEventLogService.WriteAsync("HANA", "Verbindung erfolgreich", land: land, - details: $"Schema={schema} | TSC={tsc}").GetAwaiter().GetResult(); + await _appEventLogService.WriteAsync("HANA", "Verbindung erfolgreich", land: land, + details: $"Schema={schema} | TSC={tsc}"); - var invoiceQuery = GetInvoiceQuery(schema, tsc, dateFilter); - var creditNoteQuery = GetCreditNoteQuery(schema, tsc, dateFilter); + var invoiceQuery = GetInvoiceQuery(schema); + var creditNoteQuery = GetCreditNoteQuery(schema); + var parsedDateFilter = ParseDateFilter(dateFilter); - _appEventLogService.WriteAsync("HANA", "Invoice-Query gestartet", land: land, details: invoiceQuery).GetAwaiter().GetResult(); - var invoiceRecords = ReadRecords(connection, invoiceQuery, land, "Invoice"); + await _appEventLogService.WriteAsync("HANA", "Invoice-Query gestartet", land: land, + details: BuildQueryLogDetails(invoiceQuery, schema, tsc, parsedDateFilter)); + var invoiceRecords = await ReadRecordsAsync(connection, invoiceQuery, tsc, parsedDateFilter, land, "Invoice", cancellationToken); result.AddRange(invoiceRecords); - _appEventLogService.WriteAsync("HANA", "Invoice-Query beendet", land: land, details: $"Zeilen={invoiceRecords.Count}").GetAwaiter().GetResult(); + await _appEventLogService.WriteAsync("HANA", "Invoice-Query beendet", land: land, details: $"Zeilen={invoiceRecords.Count}"); - _appEventLogService.WriteAsync("HANA", "Credit-Query gestartet", land: land, details: creditNoteQuery).GetAwaiter().GetResult(); - var creditRecords = ReadRecords(connection, creditNoteQuery, land, "Credit"); + await _appEventLogService.WriteAsync("HANA", "Credit-Query gestartet", land: land, + details: BuildQueryLogDetails(creditNoteQuery, schema, tsc, parsedDateFilter)); + var creditRecords = await ReadRecordsAsync(connection, creditNoteQuery, tsc, parsedDateFilter, land, "Credit", cancellationToken); result.AddRange(creditRecords); - _appEventLogService.WriteAsync("HANA", "Credit-Query beendet", land: land, details: $"Zeilen={creditRecords.Count}").GetAwaiter().GetResult(); + await _appEventLogService.WriteAsync("HANA", "Credit-Query beendet", land: land, details: $"Zeilen={creditRecords.Count}"); } catch (Exception ex) { - _appEventLogService.WriteAsync("HANA", "HANA-Abfrage fehlgeschlagen", "Error", land: land, details: ex.ToString()).GetAwaiter().GetResult(); + await _appEventLogService.WriteAsync("HANA", "HANA-Abfrage fehlgeschlagen", "Error", land: land, details: ex.ToString()); throw; } @@ -60,7 +65,7 @@ public class HanaQueryService : IHanaQueryService return result; } - public ConnectionTestResult TestConnectionDetailed(HanaServer server) + public async Task TestConnectionDetailedAsync(HanaServer server, CancellationToken cancellationToken = default) { var testResult = new ConnectionTestResult { @@ -71,20 +76,20 @@ public class HanaQueryService : IHanaQueryService try { - _appEventLogService.WriteAsync("HANA", "Verbindungstest gestartet", - details: testResult.ConnectionStringPreview).GetAwaiter().GetResult(); + await _appEventLogService.WriteAsync("HANA", "Verbindungstest gestartet", + details: testResult.ConnectionStringPreview); var connectionString = server.BuildConnectionString(); using var connection = new HanaConnection(connectionString); - connection.Open(); + await connection.OpenAsync(cancellationToken); testResult.Stage = "Ping-Query"; using var command = new HanaCommand("SELECT 1 FROM DUMMY", connection); - command.ExecuteScalar(); + await command.ExecuteScalarAsync(cancellationToken); testResult.Success = true; testResult.Stage = "OK"; - _appEventLogService.WriteAsync("HANA", "Verbindungstest erfolgreich", - details: testResult.ConnectionStringPreview).GetAwaiter().GetResult(); + await _appEventLogService.WriteAsync("HANA", "Verbindungstest erfolgreich", + details: testResult.ConnectionStringPreview); return testResult; } catch (Exception ex) @@ -92,24 +97,24 @@ public class HanaQueryService : IHanaQueryService testResult.Success = false; testResult.ErrorMessage = ex.Message; testResult.ExceptionType = ex.GetType().Name; - _appEventLogService.WriteAsync("HANA", "Verbindungstest fehlgeschlagen", "Error", - details: $"{testResult.ConnectionStringPreview}{Environment.NewLine}{ex}").GetAwaiter().GetResult(); + await _appEventLogService.WriteAsync("HANA", "Verbindungstest fehlgeschlagen", "Error", + details: $"{testResult.ConnectionStringPreview}{Environment.NewLine}{ex}"); return testResult; } } - public void TestConnection(HanaServer server) + public async Task TestConnectionAsync(HanaServer server, CancellationToken cancellationToken = default) { var connectionString = server.BuildConnectionString(); using var connection = new HanaConnection(connectionString); - connection.Open(); + await connection.OpenAsync(cancellationToken); } - public List GetAvailableSchemas(HanaServer server) + public async Task> GetAvailableSchemasAsync(HanaServer server, CancellationToken cancellationToken = default) { var connectionString = server.BuildConnectionString(); using var connection = new HanaConnection(connectionString); - connection.Open(); + await connection.OpenAsync(cancellationToken); const string query = """ SELECT schema_name @@ -124,10 +129,10 @@ public class HanaQueryService : IHanaQueryService """; using var command = new HanaCommand(query, connection); - using var reader = command.ExecuteReader(); + using var reader = await command.ExecuteReaderAsync(cancellationToken); var schemas = new List(); - while (reader.Read()) + while (await reader.ReadAsync(cancellationToken)) { var schema = reader["schema_name"]?.ToString()?.Trim(); if (!string.IsNullOrWhiteSpace(schema)) @@ -137,15 +142,17 @@ public class HanaQueryService : IHanaQueryService return schemas; } - private List ReadRecords(HanaConnection connection, string query, string land, string queryName) + private async Task> ReadRecordsAsync(HanaConnection connection, string query, string tsc, DateTime dateFilter, string land, string queryName, CancellationToken cancellationToken) { var records = new List(); using var command = new HanaCommand(query, connection); - using var reader = command.ExecuteReader(); + command.Parameters.Add(new HanaParameter(TscParameterName, HanaDbType.NVarChar) { Value = tsc }); + command.Parameters.Add(new HanaParameter(DateFilterParameterName, HanaDbType.Date) { Value = dateFilter.Date }); + using var reader = await command.ExecuteReaderAsync(cancellationToken); var counter = 0; - while (reader.Read()) + while (await reader.ReadAsync(cancellationToken)) { records.Add(new SalesRecord { @@ -180,18 +187,21 @@ public class HanaQueryService : IHanaQueryService counter++; if (counter % 250 == 0) { - _appEventLogService.WriteDebugAsync("HANA", $"{queryName}-Query liest Daten", land: land, - details: $"Bisher gelesene Zeilen={counter}").GetAwaiter().GetResult(); + await _appEventLogService.WriteDebugAsync("HANA", $"{queryName}-Query liest Daten", land: land, + details: $"Bisher gelesene Zeilen={counter}"); } } return records; } - private static string GetInvoiceQuery(string schema, string tsc, string dateFilter) => $@" + private static string GetInvoiceQuery(string schema) + { + var quotedSchema = QuoteIdentifier(schema); + return $@" SELECT CURRENT_TIMESTAMP AS extraction_date, - '{tsc}' AS tsc, + :{TscParameterName} AS tsc, h.""DocNum"" AS invoice_number, p.""LineNum"" AS invoice_position, h.""DocDate"" AS invoice_date, @@ -216,30 +226,34 @@ SELECT '' AS incoterms_2020, COALESCE(emp.""SlpName"", '') AS sales_responsible, CASE WHEN p.""BaseType"" = 17 - THEN (SELECT o.""DocDate"" FROM {schema}.""ORDR"" o + THEN (SELECT o.""DocDate"" FROM {quotedSchema}.""ORDR"" o WHERE o.""DocEntry"" = p.""BaseEntry"") ELSE NULL END AS order_date, 'INV' AS doc_type -FROM {schema}.""OINV"" h -INNER JOIN {schema}.""INV1"" p ON h.""DocEntry"" = p.""DocEntry"" -LEFT JOIN {schema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode"" -LEFT JOIN {schema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod"" -LEFT JOIN {schema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode"" -LEFT JOIN {schema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode"" +FROM {quotedSchema}.""OINV"" h +INNER JOIN {quotedSchema}.""INV1"" p ON h.""DocEntry"" = p.""DocEntry"" +LEFT JOIN {quotedSchema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode"" +LEFT JOIN {quotedSchema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod"" +LEFT JOIN {quotedSchema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode"" +LEFT JOIN {quotedSchema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode"" AND cust_adr.""AdresType"" = 'B' AND cust_adr.""Address"" = h.""PayToCode"" -LEFT JOIN {schema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode"" -LEFT JOIN {schema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode"" +LEFT JOIN {quotedSchema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode"" +LEFT JOIN {quotedSchema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode"" AND sup.""CardType"" = 'S' -LEFT JOIN {schema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode"" +LEFT JOIN {quotedSchema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode"" AND sup_adr.""AdresType"" = 'B' -LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode"" -WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '{dateFilter}' +LEFT JOIN {quotedSchema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode"" +WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= :{DateFilterParameterName} ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum"""; + } - private static string GetCreditNoteQuery(string schema, string tsc, string dateFilter) => $@" + private static string GetCreditNoteQuery(string schema) + { + var quotedSchema = QuoteIdentifier(schema); + return $@" SELECT CURRENT_TIMESTAMP AS extraction_date, - '{tsc}' AS tsc, + :{TscParameterName} AS tsc, h.""DocNum"" AS invoice_number, p.""LineNum"" AS invoice_position, h.""DocDate"" AS invoice_date, @@ -263,21 +277,48 @@ SELECT COALESCE(emp.""SlpName"", '') AS sales_responsible, NULL AS order_date, 'CRN' AS doc_type -FROM {schema}.""ORIN"" h -INNER JOIN {schema}.""RIN1"" p ON h.""DocEntry"" = p.""DocEntry"" -LEFT JOIN {schema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode"" -LEFT JOIN {schema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod"" -LEFT JOIN {schema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode"" -LEFT JOIN {schema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode"" +FROM {quotedSchema}.""ORIN"" h +INNER JOIN {quotedSchema}.""RIN1"" p ON h.""DocEntry"" = p.""DocEntry"" +LEFT JOIN {quotedSchema}.""OITM"" itm ON p.""ItemCode"" = itm.""ItemCode"" +LEFT JOIN {quotedSchema}.""OITB"" grp ON itm.""ItmsGrpCod"" = grp.""ItmsGrpCod"" +LEFT JOIN {quotedSchema}.""OCRD"" cust ON h.""CardCode"" = cust.""CardCode"" +LEFT JOIN {quotedSchema}.""CRD1"" cust_adr ON h.""CardCode"" = cust_adr.""CardCode"" AND cust_adr.""AdresType"" = 'B' AND cust_adr.""Address"" = h.""PayToCode"" -LEFT JOIN {schema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode"" -LEFT JOIN {schema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode"" +LEFT JOIN {quotedSchema}.""OOND"" ind ON cust.""IndustryC"" = ind.""IndCode"" +LEFT JOIN {quotedSchema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode"" AND sup.""CardType"" = 'S' -LEFT JOIN {schema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode"" +LEFT JOIN {quotedSchema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode"" AND sup_adr.""AdresType"" = 'B' -LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode"" -WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '{dateFilter}' +LEFT JOIN {quotedSchema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode"" +WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= :{DateFilterParameterName} ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum"""; + } + + private static DateTime ParseDateFilter(string dateFilter) + { + if (DateTime.TryParse(dateFilter, out var parsed)) + return parsed.Date; + + throw new InvalidOperationException($"Ungueltiger HANA-DateFilter: '{dateFilter}'. Erwartet wird ein parsebares Datum."); + } + + private static string BuildQueryLogDetails(string query, string schema, string tsc, DateTime dateFilter) + => $"{query}{Environment.NewLine}-- schema={schema}; tsc={tsc}; dateFilter={dateFilter:yyyy-MM-dd}"; + + private static string QuoteIdentifier(string identifier) + { + var value = identifier?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(value)) + throw new InvalidOperationException("HANA-Schema darf nicht leer sein."); + + foreach (var ch in value) + { + if (!(char.IsLetterOrDigit(ch) || ch == '_')) + throw new InvalidOperationException($"Ungueltiger HANA-Identifier: '{identifier}'."); + } + + return $@"""{value}"""; + } } public class ConnectionTestResult diff --git a/TrafagSalesExporter/Services/IHanaQueryService.cs b/TrafagSalesExporter/Services/IHanaQueryService.cs index 8a1ccf3..94e9473 100644 --- a/TrafagSalesExporter/Services/IHanaQueryService.cs +++ b/TrafagSalesExporter/Services/IHanaQueryService.cs @@ -4,8 +4,8 @@ namespace TrafagSalesExporter.Services; public interface IHanaQueryService { - List GetSalesRecords(HanaServer server, string schema, string tsc, string land, string dateFilter); - List GetAvailableSchemas(HanaServer server); - ConnectionTestResult TestConnectionDetailed(HanaServer server); - void TestConnection(HanaServer server); + Task> GetSalesRecordsAsync(HanaServer server, string schema, string tsc, string land, string dateFilter, CancellationToken cancellationToken = default); + Task> GetAvailableSchemasAsync(HanaServer server, CancellationToken cancellationToken = default); + Task TestConnectionDetailedAsync(HanaServer server, CancellationToken cancellationToken = default); + Task TestConnectionAsync(HanaServer server, CancellationToken cancellationToken = default); } diff --git a/TrafagSalesExporter/Services/SettingsPageService.cs b/TrafagSalesExporter/Services/SettingsPageService.cs index 23185f2..a871384 100644 --- a/TrafagSalesExporter/Services/SettingsPageService.cs +++ b/TrafagSalesExporter/Services/SettingsPageService.cs @@ -236,7 +236,7 @@ public sealed class SettingsPageService : ISettingsPageService AdditionalParams = centralServer.AdditionalParams }; - var result = await Task.Run(() => _hanaService.TestConnectionDetailed(testServer)); + var result = await _hanaService.TestConnectionDetailedAsync(testServer); return result.Success ? PageActionResult.SuccessResult($"{sourceSystem}: Zentrale HANA-Verbindung erfolgreich.") : PageActionResult.ErrorResult($"{sourceSystem}: {result.ExceptionType} - {result.ErrorMessage}"); diff --git a/TrafagSalesExporter/Services/StandortePageService.cs b/TrafagSalesExporter/Services/StandortePageService.cs index 427a836..ac62c34 100644 --- a/TrafagSalesExporter/Services/StandortePageService.cs +++ b/TrafagSalesExporter/Services/StandortePageService.cs @@ -150,7 +150,7 @@ public sealed class StandortePageService : IStandortePageService }; await _appEventLogService.WriteAsync("HANA", "Server-Test aus UI gestartet", details: testServer.GetConnectionStringPreview()); - return await Task.Run(() => _hanaService.TestConnectionDetailed(testServer)); + return await _hanaService.TestConnectionDetailedAsync(testServer); } public async Task LoadSiteEditorAsync(Site site, IEnumerable sourceSystems) @@ -263,12 +263,12 @@ public sealed class StandortePageService : IStandortePageService AdditionalParams = centralServer.AdditionalParams }; - return await Task.Run(() => _hanaService.GetAvailableSchemas(lookupServer)) - .ContinueWith(task => task.Result - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) - .ToList()); + var schemas = await _hanaService.GetAvailableSchemasAsync(lookupServer); + return schemas + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) + .ToList(); } public async Task RefreshSapEntitySetsAsync(Site site)