Добавьте файлы проекта.

This commit is contained in:
FrigaT
2026-01-05 00:29:19 +03:00
committed by FrigaT
parent 76a09d80d4
commit d0653c2098
105 changed files with 6729 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
using Microsoft.Extensions.Logging;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using SQLVision.Services.Exporters;
using System.Data;
namespace SQLVision.Services.Services;
public class ExportService : IExportService
{
private readonly ILogger<ExportService> _logger;
private readonly Dictionary<string, IExportHandler> _exportHandlers;
public ExportService(
ILogger<ExportService> logger,
IEnumerable<IExportHandler> exportHandlers)
{
_logger = logger;
_exportHandlers = exportHandlers.ToDictionary(h => h.FormatName, h => h);
}
public async Task ExportAsync(DataTable data, string filePath, ExportOptions options)
{
if (data == null) throw new ArgumentNullException(nameof(data));
var extension = Path.GetExtension(filePath).TrimStart('.').ToLower();
var format = options?.Format ?? GetFormatFromExtension(extension);
if (_exportHandlers.TryGetValue(format, out var handler))
{
try
{
await handler.ExportAsync(data, filePath, options ?? new ExportOptions());
_logger.LogInformation("Exported {Rows} rows to {FilePath} as {Format}",
data.Rows.Count, filePath, format);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error exporting to {Format}", format);
throw;
}
}
else
{
throw new NotSupportedException($"Export format '{format}' is not supported");
}
}
public async Task ExportAsync(DataSet dataSet, string filePath, ExportOptions options)
{
if (dataSet.Tables.Count == 0)
throw new InvalidOperationException("DataSet contains no tables");
if (dataSet.Tables.Count == 1)
{
await ExportAsync(dataSet.Tables[0], filePath, options);
}
else
{
// Для Excel создаем несколько листов
var extension = Path.GetExtension(filePath);
if (extension.Equals(".xlsx", StringComparison.OrdinalIgnoreCase))
{
await ExportToMultiSheetExcel(dataSet, filePath, options);
}
else
{
// Для других форматов - экспортируем каждый лист в отдельный файл
var basePath = Path.Combine(Path.GetDirectoryName(filePath)!,
Path.GetFileNameWithoutExtension(filePath));
for (int i = 0; i < dataSet.Tables.Count; i++)
{
var table = dataSet.Tables[i];
var tableFilePath = $"{basePath}_{table.TableName}_{i + 1}{extension}";
await ExportAsync(table, tableFilePath, options);
}
}
}
}
public async Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
{
var format = options?.Format ?? "Excel";
if (_exportHandlers.TryGetValue(format, out var handler))
{
return await handler.ExportToMemoryAsync(data, options ?? new ExportOptions());
}
throw new NotSupportedException($"Export format '{format}' is not supported");
}
private async Task ExportToMultiSheetExcel(DataSet dataSet, string filePath, ExportOptions options)
{
using var workbook = new ClosedXML.Excel.XLWorkbook();
for (int i = 0; i < dataSet.Tables.Count; i++)
{
var table = dataSet.Tables[i];
var worksheet = workbook.Worksheets.Add(table.TableName ?? $"Sheet{i + 1}");
// Используем ExcelExporter для записи данных
if (_exportHandlers.TryGetValue("Excel", out var excelHandler) &&
excelHandler is ExcelExporter excelExporter)
{
var excelOptions = options ?? new ExportOptions();
await excelExporter.ExportAsync(table, filePath, excelOptions);
}
}
await Task.Run(() => workbook.SaveAs(filePath));
}
private string GetFormatFromExtension(string extension) => extension switch
{
"xlsx" => "Excel",
"csv" => "CSV",
"json" => "JSON",
_ => "Excel"
};
}

View File

@@ -0,0 +1,122 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SQLVision.Core.Enums;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using System.Reflection;
namespace SQLVision.Services.Services;
public class PluginManager : IPluginManager
{
private readonly ILogger<PluginManager> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly List<ISqlVisionPlugin> _plugins = new();
public PluginManager(ILogger<PluginManager> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public void LoadPlugins(string pluginsDirectory)
{
if (!Directory.Exists(pluginsDirectory))
{
Directory.CreateDirectory(pluginsDirectory);
_logger.LogInformation("Created plugins directory: {Directory}", pluginsDirectory);
return;
}
var pluginFiles = Directory.GetFiles(pluginsDirectory, "*.dll");
_logger.LogInformation("Found {Count} plugin files", pluginFiles.Length);
foreach (var pluginFile in pluginFiles)
{
try
{
var assembly = Assembly.LoadFrom(pluginFile);
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(ISqlVisionPlugin).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
foreach (var pluginType in pluginTypes)
{
try
{
var plugin = (ISqlVisionPlugin)Activator.CreateInstance(pluginType)!;
var context = new PluginContext(_serviceProvider);
plugin.InitializeAsync(context).Wait();
_plugins.Add(plugin);
_logger.LogInformation("Loaded plugin: {Name} v{Version}", plugin.Name, plugin.Version);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to initialize plugin from type {Type}", pluginType.FullName);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load plugin from {File}", pluginFile);
}
}
}
public IEnumerable<ISqlVisionPlugin> GetPlugins() => _plugins.AsReadOnly();
public T? GetPlugin<T>() where T : ISqlVisionPlugin
=> _plugins.OfType<T>().FirstOrDefault();
public async Task BeforeExecutionAsync(ScriptMetadata script, Dictionary<string, object> parameters)
{
foreach (var plugin in _plugins)
{
try
{
await plugin.InitializeAsync(new PluginContext(_serviceProvider));
// TODO: Добавить метод BeforeExecution в ISqlVisionPlugin если нужно
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in plugin {Plugin} BeforeExecution", plugin.Name);
}
}
}
public async Task AfterExecutionAsync(ScriptMetadata script, ExecutionResult result)
{
foreach (var plugin in _plugins)
{
try
{
// TODO: Добавить метод AfterExecution в ISqlVisionPlugin если нужно
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in plugin {Plugin} AfterExecution", plugin.Name);
}
}
}
private class PluginContext : IPluginContext
{
public IServiceProvider ServiceProvider { get; }
public IConfiguration Configuration { get; }
public ILogger Logger { get; }
public PluginContext(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
Configuration = serviceProvider.GetRequiredService<IConfiguration>();
Logger = serviceProvider.GetRequiredService<ILogger<PluginManager>>();
}
public Task ShowNotificationAsync(string message, NotificationType type)
{
// TODO: Реализовать показ уведомлений через UI
Logger.LogInformation("Plugin notification ({Type}): {Message}", type, message);
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,197 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SQLVision.Core.Enums;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using System.Collections.Concurrent;
namespace SQLVision.Services.Services;
public class ScriptManager : IScriptManager, IDisposable
{
private readonly ISqlScriptParser _parser;
private readonly ILogger<ScriptManager> _logger;
private readonly FileSystemWatcher _watcher;
private readonly ConcurrentDictionary<string, ScriptMetadata> _scripts;
private readonly string _scriptsDirectory;
public event EventHandler<ScriptChangedEventArgs>? ScriptChanged;
public event EventHandler<ScriptsReloadedEventArgs>? ScriptsReloaded;
public ScriptManager(ISqlScriptParser parser, IConfiguration configuration, ILogger<ScriptManager> logger)
{
_parser = parser;
_logger = logger;
_scripts = new ConcurrentDictionary<string, ScriptMetadata>();
_scriptsDirectory = configuration["Scripts:Directory"] ?? "Scripts";
_watcher = new FileSystemWatcher
{
Path = _scriptsDirectory,
Filter = "*.sql",
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName,
EnableRaisingEvents = false
};
_watcher.Changed += OnScriptChanged;
_watcher.Created += OnScriptCreated;
_watcher.Deleted += OnScriptDeleted;
_watcher.Renamed += OnScriptRenamed;
}
public async Task<IEnumerable<ScriptMetadata>> LoadScriptsAsync(string? directory = null)
{
var targetDirectory = directory ?? _scriptsDirectory;
if (!Directory.Exists(targetDirectory))
{
Directory.CreateDirectory(targetDirectory);
_logger.LogInformation("Created scripts directory: {Directory}", targetDirectory);
return Enumerable.Empty<ScriptMetadata>();
}
var sqlFiles = Directory.GetFiles(targetDirectory, "*.sql", SearchOption.AllDirectories);
var tasks = sqlFiles.Select(LoadScriptAsync);
var results = await Task.WhenAll(tasks);
_scripts.Clear();
foreach (var script in results.Where(s => s != null))
{
_scripts[script!.FullPath] = script;
}
StartWatching();
ScriptsReloaded?.Invoke(this, new ScriptsReloadedEventArgs(results.Where(s => s != null).ToList()!));
_logger.LogInformation("Loaded {Count} scripts from {Directory}", _scripts.Count, targetDirectory);
return _scripts.Values;
}
private async Task<ScriptMetadata?> LoadScriptAsync(string filePath)
{
try
{
return await _parser.ParseAsync(filePath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading script: {FilePath}", filePath);
return null;
}
}
public async Task<ScriptMetadata> ReloadScriptAsync(string filePath)
{
try
{
var script = await LoadScriptAsync(filePath);
if (script != null)
{
_scripts[filePath] = script;
ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(filePath, ScriptChangeType.Updated, script));
}
return script!;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error reloading script: {FilePath}", filePath);
throw;
}
}
private void OnScriptChanged(object sender, FileSystemEventArgs e)
{
// Задержка для избежания многократных вызовов
Task.Delay(300).ContinueWith(async _ =>
{
try
{
var script = await ReloadScriptAsync(e.FullPath);
if (script != null)
{
_logger.LogInformation("Script changed: {FileName}", e.Name);
}
}
catch { /* Игнорируем ошибки */ }
});
}
private void OnScriptCreated(object sender, FileSystemEventArgs e)
{
Task.Delay(300).ContinueWith(async _ =>
{
try
{
var script = await LoadScriptAsync(e.FullPath);
if (script != null)
{
_scripts[e.FullPath] = script;
ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Created, script));
_logger.LogInformation("Script created: {FileName}", e.Name);
}
}
catch { /* Игнорируем */ }
});
}
private void OnScriptDeleted(object sender, FileSystemEventArgs e)
{
if (_scripts.TryRemove(e.FullPath, out var script))
{
ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Deleted, script));
_logger.LogInformation("Script deleted: {FileName}", e.Name);
}
}
private void OnScriptRenamed(object sender, RenamedEventArgs e)
{
Task.Delay(300).ContinueWith(async _ =>
{
try
{
// Удаляем старый файл
_scripts.TryRemove(e.OldFullPath, out var s);
// Загружаем новый
var script = await LoadScriptAsync(e.FullPath);
if (script != null)
{
_scripts[e.FullPath] = script;
ScriptChanged?.Invoke(this,
new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Renamed, script));
_logger.LogInformation("Script renamed: {OldName} -> {NewName}",
Path.GetFileName(e.OldFullPath), e.Name);
}
}
catch { /* Игнорируем */ }
});
}
private void StartWatching()
{
if (!_watcher.EnableRaisingEvents)
{
_watcher.EnableRaisingEvents = true;
_logger.LogDebug("Started watching directory: {Directory}", _scriptsDirectory);
}
}
public void WatchDirectory(string directory, Action<string> onScriptChanged)
{
if (_watcher.EnableRaisingEvents)
{
_watcher.EnableRaisingEvents = false;
}
_watcher.Path = directory;
_watcher.EnableRaisingEvents = true;
ScriptChanged += (sender, e) => onScriptChanged?.Invoke(e.FilePath);
}
public void Dispose()
{
_watcher?.Dispose();
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SQLVision.Core.Interfaces;
using SQLVision.Services.Exporters;
using SQLVision.Services.Parsers;
using SQLVision.Services.Services;
namespace SQLVision.Services;
public static class ServiceExtensions
{
public static IServiceCollection AddSqlVisionServices(this IServiceCollection services)
{
// Регистрация парсера
services.TryAddSingleton<ISqlScriptParser, SqlScriptParser>();
// Регистрация сервиса выполнения SQL
services.TryAddSingleton<ISqlExecutionService, SqlExecutionService>();
// Регистрация менеджера скриптов
services.TryAddSingleton<IScriptManager, ScriptManager>();
// Регистрация сервиса экспорта
services.TryAddSingleton<IExportService, ExportService>();
// Регистрация менеджера плагинов
services.TryAddSingleton<IPluginManager, PluginManager>();
// Регистрация экспортеров
services.TryAddSingleton<IExportHandler, ExcelExporter>();
services.TryAddSingleton<IExportHandler, CsvExporter>();
services.TryAddSingleton<IExportHandler, JsonExporter>();
return services;
}
}

View File

@@ -0,0 +1,305 @@
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SQLVision.Core.Enums;
using SQLVision.Core.Interfaces;
using SQLVision.Core.Models;
using SQLVision.Services.Configuration;
using System.Collections.Concurrent;
using System.Data;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
namespace SQLVision.Services.Services;
public class SqlExecutionService : ISqlExecutionService, IDisposable
{
private readonly IMemoryCache _cache;
private readonly ILogger<SqlExecutionService> _logger;
private readonly IOptions<DatabaseOptions> _options;
private readonly ConcurrentDictionary<string, Task<DataTable>> _loadingComboBoxData = new();
public SqlExecutionService(
IMemoryCache cache,
ILogger<SqlExecutionService> logger,
IOptions<DatabaseOptions> options)
{
_cache = cache;
_logger = logger;
_options = options;
}
public async Task<ExecutionResult> ExecuteAsync(
ScriptMetadata script,
Dictionary<string, object> parameters,
CancellationToken cancellationToken = default)
{
var stopwatch = Stopwatch.StartNew();
var result = new ExecutionResult
{
Parameters = new Dictionary<string, object>(parameters),
ExecutionDate = DateTime.UtcNow,
ConnectionName = script.ConnectionString
};
try
{
// Генерация ключа кэша
var cacheKey = GenerateCacheKey(script, parameters);
if (_options.Value.Cache.Enabled)
{
if (_cache.TryGetValue<ExecutionResult>(cacheKey, out var cachedResult))
{
_logger.LogDebug("Returning cached result for {Script}", script.FileName);
cachedResult!.IsFromCache = true;
cachedResult.ExecutionTime = stopwatch.Elapsed;
return cachedResult;
}
}
// Подготовка SQL с параметрами
var (processedSql, dbParameters) = PrepareSql(
script.ProcessedSql,
parameters,
script.DatabaseProvider);
result.ExecutedSql = processedSql;
// Выполнение запроса
var dataSet = await ExecuteQueryAsync(
processedSql,
dbParameters,
script.ConnectionString ?? _options.Value.DefaultConnection,
cancellationToken);
stopwatch.Stop();
result.Data = dataSet;
result.IsSuccess = true;
result.ExecutionTime = stopwatch.Elapsed;
result.RowCount = dataSet.Tables.Cast<DataTable>().Sum(t => t.Rows.Count);
result.Metrics = new Dictionary<string, object>
{
["ExecutionTimeMs"] = stopwatch.ElapsedMilliseconds,
["RowsAffected"] = result.RowCount,
["TablesCount"] = dataSet.Tables.Count
};
// Кэширование результата
if (_options.Value.Cache.Enabled && result.RowCount > 0)
{
var cacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_options.Value.Cache.DurationMinutes),
Size = CalculateDataSetSize(dataSet)
};
_cache.Set(cacheKey, result, cacheOptions);
}
_logger.LogInformation(
"Executed {Script} in {ElapsedMs}ms, returned {Rows} rows",
script.FileName, stopwatch.ElapsedMilliseconds, result.RowCount);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
result.IsSuccess = false;
result.ErrorMessage = ex.Message;
result.ExecutionTime = stopwatch.Elapsed;
_logger.LogError(ex, "Error executing script {Script}", script.FileName);
return result;
}
}
private async Task<DataSet> ExecuteQueryAsync(
string sql,
List<SqlParameter> parameters,
string connectionString,
CancellationToken cancellationToken)
{
var dataSet = new DataSet();
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await using var command = new SqlCommand(sql, connection);
command.CommandTimeout = _options.Value.CommandTimeout;
// Добавление параметров
command.Parameters.AddRange(parameters.ToArray());
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
do
{
var dataTable = new DataTable();
dataTable.Load(reader);
dataSet.Tables.Add(dataTable);
} while (!reader.IsClosed && await reader.NextResultAsync(cancellationToken));
return dataSet;
}
private (string Sql, List<SqlParameter> Parameters) PrepareSql(
string sql,
Dictionary<string, object> parameters,
DatabaseProvider provider)
{
var dbParameters = new List<SqlParameter>();
var processedSql = new StringBuilder(sql);
foreach (var (key, value) in parameters)
{
var paramName = $"@{key}";
var sqlParam = CreateSqlParameter(paramName, value);
dbParameters.Add(sqlParam);
}
return (processedSql.ToString(), dbParameters);
}
private SqlParameter CreateSqlParameter(string name, object? value)
{
var sqlParam = new SqlParameter(name, value ?? DBNull.Value);
// Автоматическое определение типа данных
if (value is DateTime dateTime)
{
sqlParam.SqlDbType = SqlDbType.DateTime2;
sqlParam.Value = dateTime;
}
else if (value is int intValue)
{
sqlParam.SqlDbType = SqlDbType.Int;
sqlParam.Value = intValue;
}
else if (value is decimal decimalValue)
{
sqlParam.SqlDbType = SqlDbType.Decimal;
sqlParam.Value = decimalValue;
}
else if (value is bool boolValue)
{
sqlParam.SqlDbType = SqlDbType.Bit;
sqlParam.Value = boolValue;
}
else if (value is string stringValue)
{
sqlParam.SqlDbType = SqlDbType.NVarChar;
sqlParam.Value = stringValue;
sqlParam.Size = Math.Min(stringValue.Length * 2, 4000); // Ограничение для NVARCHAR
}
return sqlParam;
}
public async Task<ExecutionResult> ExecuteAsync(
string sql,
Dictionary<string, object> parameters,
string connectionString,
CancellationToken cancellationToken = default)
{
var script = new ScriptMetadata
{
ProcessedSql = sql,
ConnectionString = connectionString,
DatabaseProvider = DatabaseProvider.SqlServer
};
return await ExecuteAsync(script, parameters, cancellationToken);
}
public async Task<bool> TestConnectionAsync(
string connectionString,
DatabaseProvider provider,
CancellationToken cancellationToken = default)
{
try
{
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await connection.CloseAsync();
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Connection test failed for {Provider}", provider);
return false;
}
}
public async Task<DataTable> LoadComboBoxDataAsync(
string query,
string connectionString,
DatabaseProvider provider,
CancellationToken cancellationToken = default)
{
var cacheKey = $"ComboBox_{provider}_{connectionString}_{query.GetHashCode()}";
return await _loadingComboBoxData.GetOrAdd(cacheKey, async key =>
{
try
{
await using var connection = new SqlConnection(connectionString);
await using var command = new SqlCommand(query, connection);
command.CommandTimeout = 30;
await connection.OpenAsync(cancellationToken);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
var dataTable = new DataTable();
dataTable.Load(reader);
return dataTable;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load combo box data for query: {Query}", query);
throw;
}
finally
{
_loadingComboBoxData.TryRemove(key, out _);
}
});
}
private string GenerateCacheKey(ScriptMetadata script, Dictionary<string, object> parameters)
{
using var sha256 = SHA256.Create();
// Создаем строку для хэширования
var keyBuilder = new StringBuilder();
keyBuilder.Append(script.Id);
foreach (var param in parameters.OrderBy(p => p.Key))
{
keyBuilder.Append($"|{param.Key}={param.Value}");
}
var keyData = keyBuilder.ToString();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData));
return Convert.ToBase64String(hash);
}
private long CalculateDataSetSize(DataSet dataSet)
{
long size = 0;
foreach (DataTable table in dataSet.Tables)
{
// Примерный расчет размера: кол-во строк * кол-во столбцов * средний размер
size += table.Rows.Count * table.Columns.Count * 64; // 64 байта на ячейку
}
return size;
}
public void Dispose()
{
// Очищаем кэш загрузки данных для ComboBox
_loadingComboBoxData.Clear();
}
}