erp-platform/api/modules/Erp.SqlQueryManager/Erp.SqlQueryManager.Domain/Services/SqlExecutorService.cs
2025-12-06 01:38:21 +03:00

301 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Erp.Platform.DynamicData;
using Erp.Platform.Queries;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp;
using Volo.Abp.Domain.Services;
namespace Erp.SqlQueryManager.Domain.Services;
public class SqlExecutorService : DomainService, ISqlExecutorService
{
private readonly IDataSourceManager _dataSourceManager;
private readonly IServiceProvider _serviceProvider;
public SqlExecutorService(
IDataSourceManager dataSourceManager,
IServiceProvider serviceProvider)
{
_dataSourceManager = dataSourceManager;
_serviceProvider = serviceProvider;
}
private async Task<IDynamicDataRepository> GetRepositoryAsync(string dataSourceCode)
{
// Get DataSource to determine database type
var dataSource = await _dataSourceManager.GetDataSourceAsync(
CurrentTenant.IsAvailable,
dataSourceCode);
if (dataSource == null)
{
throw new BusinessException("SqlQueryManager:DataSourceNotFound")
.WithData("DataSourceCode", dataSourceCode);
}
// Get appropriate repository based on database type
// For now, using MS SQL Server repository
var repository = _serviceProvider.GetKeyedService<IDynamicDataRepository>("Ms");
if (repository == null)
{
throw new BusinessException("SqlQueryManager:RepositoryNotFound")
.WithData("DatabaseType", "Ms");
}
return repository;
}
public async Task<SqlExecutionResult> ExecuteQueryAsync(
string sql,
string dataSourceCode,
Dictionary<string, object> parameters = null)
{
var stopwatch = Stopwatch.StartNew();
var result = new SqlExecutionResult();
try
{
var connectionString = await _dataSourceManager.GetConnectionStringAsync(
CurrentTenant.IsAvailable,
dataSourceCode);
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new BusinessException("SqlQueryManager:InvalidConnectionString")
.WithData("DataSourceCode", dataSourceCode);
}
var repository = await GetRepositoryAsync(dataSourceCode);
var data = await repository.QueryAsync(sql, connectionString, parameters);
stopwatch.Stop();
result.Success = true;
result.Data = data;
result.RowsAffected = data?.Count() ?? 0;
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Message = $"Query executed successfully.";
}
catch (Exception ex)
{
stopwatch.Stop();
result.Success = false;
result.Message = $"Query execution failed: {ex.Message}";
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Metadata["ErrorDetail"] = ex.ToString();
}
return result;
}
public async Task<SqlExecutionResult> ExecuteNonQueryAsync(
string sql,
string dataSourceCode,
Dictionary<string, object> parameters = null)
{
var stopwatch = Stopwatch.StartNew();
var result = new SqlExecutionResult();
try
{
var connectionString = await _dataSourceManager.GetConnectionStringAsync(
CurrentTenant.IsAvailable,
dataSourceCode);
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new BusinessException("SqlQueryManager:InvalidConnectionString")
.WithData("DataSourceCode", dataSourceCode);
}
var repository = await GetRepositoryAsync(dataSourceCode);
var rowsAffected = await repository.ExecuteAsync(sql, connectionString, parameters);
stopwatch.Stop();
result.Success = true;
result.RowsAffected = rowsAffected;
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Message = $"Command executed successfully. Rows affected: {rowsAffected}";
}
catch (Exception ex)
{
stopwatch.Stop();
result.Success = false;
result.Message = $"Command execution failed: {ex.Message}";
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Metadata["ErrorDetail"] = ex.ToString();
}
return result;
}
public async Task<SqlExecutionResult> ExecuteScalarAsync<T>(
string sql,
string dataSourceCode,
Dictionary<string, object> parameters = null)
{
var stopwatch = Stopwatch.StartNew();
var result = new SqlExecutionResult();
try
{
var connectionString = await _dataSourceManager.GetConnectionStringAsync(
CurrentTenant.IsAvailable,
dataSourceCode);
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new BusinessException("SqlQueryManager:InvalidConnectionString")
.WithData("DataSourceCode", dataSourceCode);
}
var repository = await GetRepositoryAsync(dataSourceCode);
var scalarValue = await repository.ExecuteScalarAsync<T>(sql, connectionString, parameters);
stopwatch.Stop();
result.Success = true;
result.Data = new[] { new { Value = scalarValue } };
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Message = "Scalar query executed successfully";
result.Metadata["ScalarValue"] = scalarValue;
}
catch (Exception ex)
{
stopwatch.Stop();
result.Success = false;
result.Message = $"Scalar query execution failed: {ex.Message}";
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
result.Metadata["ErrorDetail"] = ex.ToString();
}
return result;
}
public async Task<SqlExecutionResult> DeployStoredProcedureAsync(
string procedureBody,
string dataSourceCode)
{
return await ExecuteNonQueryAsync(procedureBody, dataSourceCode);
}
public async Task<SqlExecutionResult> DeployViewAsync(
string viewDefinition,
string dataSourceCode)
{
return await ExecuteNonQueryAsync(viewDefinition, dataSourceCode);
}
public async Task<SqlExecutionResult> DeployFunctionAsync(
string functionBody,
string dataSourceCode)
{
return await ExecuteNonQueryAsync(functionBody, dataSourceCode);
}
public async Task<bool> CheckObjectExistsAsync(
string objectName,
string objectType,
string dataSourceCode,
string schemaName = "dbo")
{
try
{
var sql = $@"
SELECT COUNT(*)
FROM sys.objects o
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
WHERE s.name = @SchemaName
AND o.name = @ObjectName
AND o.type_desc = @ObjectType";
var parameters = new Dictionary<string, object>
{
{ "SchemaName", schemaName },
{ "ObjectName", objectName },
{ "ObjectType", objectType.ToUpperInvariant() }
};
var result = await ExecuteScalarAsync<int>(sql, dataSourceCode, parameters);
return result.Success && result.Metadata.ContainsKey("ScalarValue")
&& Convert.ToInt32(result.Metadata["ScalarValue"]) > 0;
}
catch
{
return false;
}
}
public async Task<SqlExecutionResult> DropObjectAsync(
string objectName,
string objectType,
string dataSourceCode,
string schemaName = "dbo")
{
var dropCommand = objectType.ToUpperInvariant() switch
{
"SQL_STORED_PROCEDURE" => $"DROP PROCEDURE IF EXISTS [{schemaName}].[{objectName}]",
"VIEW" => $"DROP VIEW IF EXISTS [{schemaName}].[{objectName}]",
"SQL_SCALAR_FUNCTION" => $"DROP FUNCTION IF EXISTS [{schemaName}].[{objectName}]",
"SQL_TABLE_VALUED_FUNCTION" => $"DROP FUNCTION IF EXISTS [{schemaName}].[{objectName}]",
"SQL_INLINE_TABLE_VALUED_FUNCTION" => $"DROP FUNCTION IF EXISTS [{schemaName}].[{objectName}]",
_ => throw new BusinessException("SqlQueryManager:UnsupportedObjectType")
.WithData("ObjectType", objectType)
};
return await ExecuteNonQueryAsync(dropCommand, dataSourceCode);
}
public Task<(bool IsValid, string ErrorMessage)> ValidateSqlAsync(string sql)
{
try
{
if (string.IsNullOrWhiteSpace(sql))
{
return Task.FromResult((false, "SQL query is empty"));
}
// Basic validation - check for dangerous keywords
var dangerousPatterns = new[]
{
@"\bDROP\s+DATABASE\b",
@"\bDROP\s+SCHEMA\b",
@"\bTRUNCATE\s+TABLE\b",
@"\bALTER\s+DATABASE\b",
@"\bSHUTDOWN\b",
@"\bxp_cmdshell\b"
};
foreach (var pattern in dangerousPatterns)
{
if (Regex.IsMatch(sql, pattern, RegexOptions.IgnoreCase))
{
return Task.FromResult((false, $"SQL contains potentially dangerous command: {pattern}"));
}
}
// Check for balanced parentheses
var openCount = sql.Count(c => c == '(');
var closeCount = sql.Count(c => c == ')');
if (openCount != closeCount)
{
return Task.FromResult((false, "Unbalanced parentheses in SQL"));
}
return Task.FromResult((true, string.Empty));
}
catch (Exception ex)
{
return Task.FromResult((false, $"Validation error: {ex.Message}"));
}
}
}