Skip to content

Commit d357941

Browse files
merge: integrate BD-847 (EF Core, connection flags, entity fixes) into main
Resolve conflicts between BD-845 YAML config and BD-847 EF Core migration. Keep YAML config structure from BD-845, add connection enable flags and recipients to YAML files, use EF Core DbContext from BD-847. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents 071212b + 6956dd0 commit d357941

33 files changed

+2008
-373
lines changed

OrderMonitor_ENV.development.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
# Non-sensitive values for local development.
33
# Sensitive values (passwords, keys) should be set as OS environment variables.
44

5+
# ===== Database Connection Enable/Disable =====
6+
SQL_CONNECTIONENABLE: "true"
7+
MYSQL_CONNECTIONENABLE: "false"
8+
POSTGRES_CONNECTIONENABLE: "false"
9+
510
# ===== Database Configuration =====
611
Database__Provider: "sqlserver"
712
Database__ConnectionString: "Server=localhost,1433;Database=PrinterPix_BO_Live;User Id=sa;Password={ENCRYPTED};TrustServerCertificate=True;ApplicationIntent=ReadOnly;"
@@ -20,7 +25,7 @@ SmtpSettings__UseSsl: "true"
2025

2126
# ===== Alert Configuration =====
2227
Alerts__Enabled: "true"
23-
Alerts__Recipients: "ranganathan.e@syncoms.com"
28+
Alerts__Recipients: "ranganathan.e@syncoms.com,muhammad.zakir@syncoms.co.uk,yarik.sobanski@syncoms.co.uk,michael.sobanski@syncoms.co.uk"
2429
Alerts__SubjectPrefix: "[Order Monitor DEV]"
2530

2631
# ===== Scanner Configuration =====

OrderMonitor_ENV.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
# Key naming: Use double-underscore (__) as hierarchy separator.
66
# Example: Database__ConnectionString maps to IConfiguration["Database:ConnectionString"]
77

8+
# ===== Database Connection Enable/Disable =====
9+
SQL_CONNECTIONENABLE: "true" # Enable SQL Server connection
10+
MYSQL_CONNECTIONENABLE: "false" # Enable MySQL connection
11+
POSTGRES_CONNECTIONENABLE: "false" # Enable PostgreSQL connection
12+
813
# ===== Database Configuration =====
9-
Database__Provider: "sqlserver" # sqlserver | mysql | postgresql
14+
Database__Provider: "sqlserver" # sqlserver | mysql | postgresql (fallback if no enable flags set)
1015
Database__ConnectionString: "" # Full connection string (without password if using EncryptedPassword)
1116
Database__EncryptedPassword: "" # Base64-encoded AES-encrypted password
1217
Database__EncryptionKey: "" # AES-256 key for decrypting EncryptedPassword (32 chars)

src/OrderMonitor.Api/Controllers/DiagnosticsController.cs

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using Dapper;
21
using Microsoft.AspNetCore.Mvc;
32
using OrderMonitor.Core.Interfaces;
43

@@ -12,12 +11,12 @@ namespace OrderMonitor.Api.Controllers;
1211
[Produces("application/json")]
1312
public class DiagnosticsController : ControllerBase
1413
{
15-
private readonly IDbConnectionFactory _connectionFactory;
14+
private readonly IDiagnosticsService _diagnosticsService;
1615
private readonly ILogger<DiagnosticsController> _logger;
1716

18-
public DiagnosticsController(IDbConnectionFactory connectionFactory, ILogger<DiagnosticsController> logger)
17+
public DiagnosticsController(IDiagnosticsService diagnosticsService, ILogger<DiagnosticsController> logger)
1918
{
20-
_connectionFactory = connectionFactory;
19+
_diagnosticsService = diagnosticsService;
2120
_logger = logger;
2221
}
2322

@@ -29,9 +28,7 @@ public async Task<IActionResult> GetOrderTables()
2928
{
3029
try
3130
{
32-
using var conn = _connectionFactory.CreateConnection();
33-
var tables = await conn.QueryAsync<string>(
34-
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE '%Order%' ORDER BY TABLE_NAME");
31+
var tables = await _diagnosticsService.GetTablesAsync("Order");
3532
return Ok(tables);
3633
}
3734
catch (Exception ex)
@@ -48,13 +45,7 @@ public async Task<IActionResult> GetTableColumns(string tableName)
4845
{
4946
try
5047
{
51-
using var conn = _connectionFactory.CreateConnection();
52-
var columns = await conn.QueryAsync<dynamic>(
53-
@"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH
54-
FROM INFORMATION_SCHEMA.COLUMNS
55-
WHERE TABLE_NAME = @TableName
56-
ORDER BY ORDINAL_POSITION",
57-
new { TableName = tableName });
48+
var columns = await _diagnosticsService.GetColumnsAsync(tableName);
5849
return Ok(columns);
5950
}
6051
catch (Exception ex)
@@ -78,9 +69,8 @@ public async Task<IActionResult> RunQuery([FromBody] QueryRequest request)
7869

7970
try
8071
{
81-
using var conn = _connectionFactory.CreateConnection();
82-
var results = await conn.QueryAsync<dynamic>(request.Sql);
83-
return Ok(results.Take(100)); // Limit to 100 rows
72+
var results = await _diagnosticsService.ExecuteQueryAsync(request.Sql);
73+
return Ok(results);
8474
}
8575
catch (Exception ex)
8676
{

src/OrderMonitor.Api/appsettings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
{
2+
"DatabaseConnections": {
3+
"SqlConnectionEnable": true,
4+
"MySqlConnectionEnable": false,
5+
"PostgresConnectionEnable": false
6+
},
7+
28
"StatusThresholds": {
39
"PrepStatuses": {
410
"MinStatusId": 3001,

src/OrderMonitor.Core/Interfaces/IDbConnectionFactory.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace OrderMonitor.Core.Interfaces;
2+
3+
/// <summary>
4+
/// Service interface for database diagnostics and schema discovery.
5+
/// </summary>
6+
public interface IDiagnosticsService
7+
{
8+
/// <summary>
9+
/// Gets table names matching a pattern.
10+
/// </summary>
11+
Task<IEnumerable<string>> GetTablesAsync(string pattern, CancellationToken cancellationToken = default);
12+
13+
/// <summary>
14+
/// Gets column information for a specific table.
15+
/// </summary>
16+
Task<IEnumerable<ColumnInfo>> GetColumnsAsync(string tableName, CancellationToken cancellationToken = default);
17+
18+
/// <summary>
19+
/// Executes a read-only query and returns results.
20+
/// </summary>
21+
Task<IEnumerable<IDictionary<string, object?>>> ExecuteQueryAsync(string sql, CancellationToken cancellationToken = default);
22+
}
23+
24+
/// <summary>
25+
/// Represents column metadata for diagnostics.
26+
/// </summary>
27+
public class ColumnInfo
28+
{
29+
public string ColumnName { get; set; } = string.Empty;
30+
public string DataType { get; set; } = string.Empty;
31+
public string IsNullable { get; set; } = string.Empty;
32+
public int? CharacterMaximumLength { get; set; }
33+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace OrderMonitor.Infrastructure.Data;
5+
6+
/// <summary>
7+
/// Supported database providers for multi-database support.
8+
/// </summary>
9+
public enum DatabaseProvider
10+
{
11+
SqlServer,
12+
MySql,
13+
PostgreSql
14+
}
15+
16+
/// <summary>
17+
/// Factory for configuring the DbContext with the appropriate database provider.
18+
/// </summary>
19+
public static class DatabaseProviderFactory
20+
{
21+
private static readonly HashSet<string> ValidProviders = new(StringComparer.OrdinalIgnoreCase)
22+
{
23+
"SqlServer", "MySql", "PostgreSql"
24+
};
25+
26+
/// <summary>
27+
/// Parses a provider string into a DatabaseProvider enum value.
28+
/// </summary>
29+
public static DatabaseProvider ParseProvider(string provider)
30+
{
31+
if (string.IsNullOrWhiteSpace(provider))
32+
throw new ArgumentException("Database provider cannot be null or empty.", nameof(provider));
33+
34+
return provider.Trim().ToLowerInvariant() switch
35+
{
36+
"sqlserver" => DatabaseProvider.SqlServer,
37+
"mysql" => DatabaseProvider.MySql,
38+
"postgresql" or "postgres" => DatabaseProvider.PostgreSql,
39+
_ => throw new ArgumentException(
40+
$"Invalid database provider '{provider}'. Allowed values: {string.Join(", ", ValidProviders)}.",
41+
nameof(provider))
42+
};
43+
}
44+
45+
/// <summary>
46+
/// Adds the OrderMonitorDbContext to the service collection with the specified provider.
47+
/// </summary>
48+
public static IServiceCollection AddOrderMonitorDbContext(
49+
this IServiceCollection services,
50+
DatabaseProvider provider,
51+
string connectionString)
52+
{
53+
if (string.IsNullOrWhiteSpace(connectionString))
54+
throw new ArgumentException("Connection string cannot be null or empty.", nameof(connectionString));
55+
56+
services.AddDbContext<OrderMonitorDbContext>(options =>
57+
{
58+
ConfigureProvider(options, provider, connectionString);
59+
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
60+
});
61+
62+
return services;
63+
}
64+
65+
/// <summary>
66+
/// Configures the DbContextOptionsBuilder for the specified provider.
67+
/// </summary>
68+
public static void ConfigureProvider(
69+
DbContextOptionsBuilder options,
70+
DatabaseProvider provider,
71+
string connectionString)
72+
{
73+
switch (provider)
74+
{
75+
case DatabaseProvider.SqlServer:
76+
options.UseSqlServer(connectionString);
77+
break;
78+
79+
case DatabaseProvider.MySql:
80+
var serverVersion = ServerVersion.AutoDetect(connectionString);
81+
options.UseMySql(connectionString, serverVersion);
82+
break;
83+
84+
case DatabaseProvider.PostgreSql:
85+
options.UseNpgsql(connectionString);
86+
break;
87+
88+
default:
89+
throw new ArgumentOutOfRangeException(nameof(provider), provider,
90+
$"Unsupported database provider: {provider}");
91+
}
92+
}
93+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using OrderMonitor.Core.Interfaces;
3+
4+
namespace OrderMonitor.Infrastructure.Data;
5+
6+
/// <summary>
7+
/// EF Core implementation of database diagnostics.
8+
/// Uses raw SQL for schema introspection queries.
9+
/// </summary>
10+
public class DiagnosticsService : IDiagnosticsService
11+
{
12+
private readonly OrderMonitorDbContext _dbContext;
13+
14+
public DiagnosticsService(OrderMonitorDbContext dbContext)
15+
{
16+
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
17+
}
18+
19+
/// <inheritdoc />
20+
public async Task<IEnumerable<string>> GetTablesAsync(
21+
string pattern,
22+
CancellationToken cancellationToken = default)
23+
{
24+
// Use INFORMATION_SCHEMA which is supported by SQL Server, MySQL, and PostgreSQL
25+
var tables = await _dbContext.Database
26+
.SqlQueryRaw<string>(
27+
"SELECT TABLE_NAME AS Value FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE {0} ORDER BY TABLE_NAME",
28+
$"%{pattern}%")
29+
.ToListAsync(cancellationToken);
30+
31+
return tables;
32+
}
33+
34+
/// <inheritdoc />
35+
public async Task<IEnumerable<ColumnInfo>> GetColumnsAsync(
36+
string tableName,
37+
CancellationToken cancellationToken = default)
38+
{
39+
var columns = await _dbContext.Database
40+
.SqlQueryRaw<ColumnInfo>(
41+
@"SELECT COLUMN_NAME AS ColumnName, DATA_TYPE AS DataType,
42+
IS_NULLABLE AS IsNullable, CHARACTER_MAXIMUM_LENGTH AS CharacterMaximumLength
43+
FROM INFORMATION_SCHEMA.COLUMNS
44+
WHERE TABLE_NAME = {0}
45+
ORDER BY ORDINAL_POSITION",
46+
tableName)
47+
.ToListAsync(cancellationToken);
48+
49+
return columns;
50+
}
51+
52+
/// <inheritdoc />
53+
public async Task<IEnumerable<IDictionary<string, object?>>> ExecuteQueryAsync(
54+
string sql,
55+
CancellationToken cancellationToken = default)
56+
{
57+
var results = new List<IDictionary<string, object?>>();
58+
59+
var connection = _dbContext.Database.GetDbConnection();
60+
await connection.OpenAsync(cancellationToken);
61+
62+
try
63+
{
64+
using var command = connection.CreateCommand();
65+
command.CommandText = sql;
66+
67+
using var reader = await command.ExecuteReaderAsync(cancellationToken);
68+
var columnNames = Enumerable.Range(0, reader.FieldCount)
69+
.Select(reader.GetName)
70+
.ToList();
71+
72+
var count = 0;
73+
while (await reader.ReadAsync(cancellationToken) && count < 100)
74+
{
75+
var row = new Dictionary<string, object?>();
76+
foreach (var col in columnNames)
77+
{
78+
var value = reader[col];
79+
row[col] = value == DBNull.Value ? null : value;
80+
}
81+
results.Add(row);
82+
count++;
83+
}
84+
}
85+
finally
86+
{
87+
await connection.CloseAsync();
88+
}
89+
90+
return results;
91+
}
92+
}

0 commit comments

Comments
 (0)