Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 7 additions & 17 deletions src/OrderMonitor.Api/Controllers/DiagnosticsController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Dapper;
using Microsoft.AspNetCore.Mvc;
using OrderMonitor.Core.Interfaces;

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

public DiagnosticsController(IDbConnectionFactory connectionFactory, ILogger<DiagnosticsController> logger)
public DiagnosticsController(IDiagnosticsService diagnosticsService, ILogger<DiagnosticsController> logger)
{
_connectionFactory = connectionFactory;
_diagnosticsService = diagnosticsService;
_logger = logger;
}

Expand All @@ -29,9 +28,7 @@ public async Task<IActionResult> GetOrderTables()
{
try
{
using var conn = _connectionFactory.CreateConnection();
var tables = await conn.QueryAsync<string>(
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE '%Order%' ORDER BY TABLE_NAME");
var tables = await _diagnosticsService.GetTablesAsync("Order");
return Ok(tables);
}
catch (Exception ex)
Expand All @@ -48,13 +45,7 @@ public async Task<IActionResult> GetTableColumns(string tableName)
{
try
{
using var conn = _connectionFactory.CreateConnection();
var columns = await conn.QueryAsync<dynamic>(
@"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @TableName
ORDER BY ORDINAL_POSITION",
new { TableName = tableName });
var columns = await _diagnosticsService.GetColumnsAsync(tableName);
return Ok(columns);
}
catch (Exception ex)
Expand All @@ -78,9 +69,8 @@ public async Task<IActionResult> RunQuery([FromBody] QueryRequest request)

try
{
using var conn = _connectionFactory.CreateConnection();
var results = await conn.QueryAsync<dynamic>(request.Sql);
return Ok(results.Take(100)); // Limit to 100 rows
var results = await _diagnosticsService.ExecuteQueryAsync(request.Sql);
return Ok(results);
}
catch (Exception ex)
{
Expand Down
14 changes: 0 additions & 14 deletions src/OrderMonitor.Core/Interfaces/IDbConnectionFactory.cs

This file was deleted.

33 changes: 33 additions & 0 deletions src/OrderMonitor.Core/Interfaces/IDiagnosticsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace OrderMonitor.Core.Interfaces;

/// <summary>
/// Service interface for database diagnostics and schema discovery.
/// </summary>
public interface IDiagnosticsService
{
/// <summary>
/// Gets table names matching a pattern.
/// </summary>
Task<IEnumerable<string>> GetTablesAsync(string pattern, CancellationToken cancellationToken = default);

/// <summary>
/// Gets column information for a specific table.
/// </summary>
Task<IEnumerable<ColumnInfo>> GetColumnsAsync(string tableName, CancellationToken cancellationToken = default);

/// <summary>
/// Executes a read-only query and returns results.
/// </summary>
Task<IEnumerable<IDictionary<string, object?>>> ExecuteQueryAsync(string sql, CancellationToken cancellationToken = default);
}

/// <summary>
/// Represents column metadata for diagnostics.
/// </summary>
public class ColumnInfo
{
public string ColumnName { get; set; } = string.Empty;
public string DataType { get; set; } = string.Empty;
public string IsNullable { get; set; } = string.Empty;
public int? CharacterMaximumLength { get; set; }
}
93 changes: 93 additions & 0 deletions src/OrderMonitor.Infrastructure/Data/DatabaseProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace OrderMonitor.Infrastructure.Data;

/// <summary>
/// Supported database providers for multi-database support.
/// </summary>
public enum DatabaseProvider
{
SqlServer,
MySql,
PostgreSql
}

/// <summary>
/// Factory for configuring the DbContext with the appropriate database provider.
/// </summary>
public static class DatabaseProviderFactory
{
private static readonly HashSet<string> ValidProviders = new(StringComparer.OrdinalIgnoreCase)
{
"SqlServer", "MySql", "PostgreSql"
};

/// <summary>
/// Parses a provider string into a DatabaseProvider enum value.
/// </summary>
public static DatabaseProvider ParseProvider(string provider)
{
if (string.IsNullOrWhiteSpace(provider))
throw new ArgumentException("Database provider cannot be null or empty.", nameof(provider));

return provider.Trim().ToLowerInvariant() switch
{
"sqlserver" => DatabaseProvider.SqlServer,
"mysql" => DatabaseProvider.MySql,
"postgresql" or "postgres" => DatabaseProvider.PostgreSql,
_ => throw new ArgumentException(
$"Invalid database provider '{provider}'. Allowed values: {string.Join(", ", ValidProviders)}.",
nameof(provider))
};
}

/// <summary>
/// Adds the OrderMonitorDbContext to the service collection with the specified provider.
/// </summary>
public static IServiceCollection AddOrderMonitorDbContext(
this IServiceCollection services,
DatabaseProvider provider,
string connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException("Connection string cannot be null or empty.", nameof(connectionString));

services.AddDbContext<OrderMonitorDbContext>(options =>
{
ConfigureProvider(options, provider, connectionString);
options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});

return services;
}

/// <summary>
/// Configures the DbContextOptionsBuilder for the specified provider.
/// </summary>
public static void ConfigureProvider(
DbContextOptionsBuilder options,
DatabaseProvider provider,
string connectionString)
{
switch (provider)
{
case DatabaseProvider.SqlServer:
options.UseSqlServer(connectionString);
break;

case DatabaseProvider.MySql:
var serverVersion = ServerVersion.AutoDetect(connectionString);
options.UseMySql(connectionString, serverVersion);
break;

case DatabaseProvider.PostgreSql:
options.UseNpgsql(connectionString);
break;

default:
throw new ArgumentOutOfRangeException(nameof(provider), provider,
$"Unsupported database provider: {provider}");
}
}
}
92 changes: 92 additions & 0 deletions src/OrderMonitor.Infrastructure/Data/DiagnosticsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Microsoft.EntityFrameworkCore;
using OrderMonitor.Core.Interfaces;

namespace OrderMonitor.Infrastructure.Data;

/// <summary>
/// EF Core implementation of database diagnostics.
/// Uses raw SQL for schema introspection queries.
/// </summary>
public class DiagnosticsService : IDiagnosticsService
{
private readonly OrderMonitorDbContext _dbContext;

public DiagnosticsService(OrderMonitorDbContext dbContext)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
}

/// <inheritdoc />
public async Task<IEnumerable<string>> GetTablesAsync(
string pattern,
CancellationToken cancellationToken = default)
{
// Use INFORMATION_SCHEMA which is supported by SQL Server, MySQL, and PostgreSQL
var tables = await _dbContext.Database
.SqlQueryRaw<string>(
"SELECT TABLE_NAME AS Value FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE {0} ORDER BY TABLE_NAME",
$"%{pattern}%")
.ToListAsync(cancellationToken);

return tables;
}

/// <inheritdoc />
public async Task<IEnumerable<ColumnInfo>> GetColumnsAsync(
string tableName,
CancellationToken cancellationToken = default)
{
var columns = await _dbContext.Database
.SqlQueryRaw<ColumnInfo>(
@"SELECT COLUMN_NAME AS ColumnName, DATA_TYPE AS DataType,
IS_NULLABLE AS IsNullable, CHARACTER_MAXIMUM_LENGTH AS CharacterMaximumLength
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = {0}
ORDER BY ORDINAL_POSITION",
tableName)
.ToListAsync(cancellationToken);

return columns;
}

/// <inheritdoc />
public async Task<IEnumerable<IDictionary<string, object?>>> ExecuteQueryAsync(
string sql,
CancellationToken cancellationToken = default)
{
var results = new List<IDictionary<string, object?>>();

var connection = _dbContext.Database.GetDbConnection();
await connection.OpenAsync(cancellationToken);

try
{
using var command = connection.CreateCommand();
command.CommandText = sql;

using var reader = await command.ExecuteReaderAsync(cancellationToken);
var columnNames = Enumerable.Range(0, reader.FieldCount)
.Select(reader.GetName)
.ToList();

var count = 0;
while (await reader.ReadAsync(cancellationToken) && count < 100)
{
var row = new Dictionary<string, object?>();
foreach (var col in columnNames)
{
var value = reader[col];
row[col] = value == DBNull.Value ? null : value;
}
results.Add(row);
count++;
}
}
finally
{
await connection.CloseAsync();
}

return results;
}
}
Loading