From a4e6e75e55adbaeed3adda61eedbf6675e28d02a Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 15 Mar 2025 01:26:11 +0000 Subject: [PATCH 1/6] Adds SQL injection prevention with the use of parameters in the SqlEvaluator. --- src/modules/Elsa.Sql.MySql/MySqlClient.cs | 33 +++++++++++++---- .../Elsa.Sql.PostgreSql/PostgreSqlClient.cs | 33 +++++++++++++---- .../Elsa.Sql.SqlServer/SqlServerClient.cs | 33 +++++++++++++---- src/modules/Elsa.Sql.Sqlite/SqliteClient.cs | 33 +++++++++++++---- src/modules/Elsa.Sql/Client/ISqlClient.cs | 13 +++---- .../Elsa.Sql/Contracts/ISqlEvaluator.cs | 5 +-- src/modules/Elsa.Sql/Elsa.Sql.csproj | 1 + src/modules/Elsa.Sql/Models/EvaluatedQuery.cs | 35 +++++++++++++++++++ src/modules/Elsa.Sql/Services/SqlEvaluator.cs | 14 +++++--- 9 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 src/modules/Elsa.Sql/Models/EvaluatedQuery.cs diff --git a/src/modules/Elsa.Sql.MySql/MySqlClient.cs b/src/modules/Elsa.Sql.MySql/MySqlClient.cs index 60d84fd3e..f8a17c61e 100644 --- a/src/modules/Elsa.Sql.MySql/MySqlClient.cs +++ b/src/modules/Elsa.Sql.MySql/MySqlClient.cs @@ -1,6 +1,7 @@ using MySql.Data.MySqlClient; using Elsa.Sql.Client; using System.Data; +using Elsa.Sql.Models; namespace Elsa.Sql.MySql; @@ -9,7 +10,7 @@ public class MySqlClient : BaseSqlClient, ISqlClient private string? _connectionString; /// - /// MySql client implimentation. + /// MySql client implementation. /// /// public MySqlClient(string? connectionString) => _connectionString = connectionString; @@ -17,11 +18,12 @@ public class MySqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteCommandAsync(string sqlCommand) + public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) { using var connection = new MySqlConnection(_connectionString); connection.Open(); - var command = new MySqlCommand(sqlCommand, connection); + var command = new MySqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); return result; @@ -30,11 +32,12 @@ public class MySqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteScalarAsync(string sqlQuery) + public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) { using var connection = new MySqlConnection(_connectionString); connection.Open(); - var command = new MySqlCommand(sqlQuery, connection); + var command = new MySqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); return result; @@ -43,13 +46,29 @@ public class MySqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteQueryAsync(string sqlQuery) + public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) { using var connection = new MySqlConnection(_connectionString); connection.Open(); - var command = new MySqlCommand(sqlQuery, connection); + var command = new MySqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); return await Task.FromResult(ReadAsDataSet(reader)); } + + /// + /// Inject parameters into the query to prevent SQL injection. + /// + /// Command to add the parameters to + /// Parameters to add + /// + private MySqlCommand AddParameters(MySqlCommand command, Dictionary parameters) + { + foreach (var param in parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); + } + return command; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs index 495439f30..48d54101a 100644 --- a/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs +++ b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs @@ -1,6 +1,7 @@ using Npgsql; using Elsa.Sql.Client; using System.Data; +using Elsa.Sql.Models; namespace Elsa.Sql.PostgreSql; @@ -9,7 +10,7 @@ public class PostgreSqlClient : BaseSqlClient, ISqlClient private string? _connectionString; /// - /// PostgreSQL client implimentation. + /// PostgreSQL client implementation. /// /// public PostgreSqlClient(string? connectionString) => _connectionString = connectionString; @@ -17,11 +18,12 @@ public class PostgreSqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteCommandAsync(string sqlCommand) + public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) { using var connection = new NpgsqlConnection(_connectionString); connection.Open(); - var command = new NpgsqlCommand(sqlCommand, connection); + var command = new NpgsqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); return result; @@ -30,11 +32,12 @@ public class PostgreSqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteScalarAsync(string sqlQuery) + public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) { using var connection = new NpgsqlConnection(_connectionString); connection.Open(); - var command = new NpgsqlCommand(sqlQuery, connection); + var command = new NpgsqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); return result; @@ -43,13 +46,29 @@ public class PostgreSqlClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteQueryAsync(string sqlQuery) + public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) { using var connection = new NpgsqlConnection(_connectionString); connection.Open(); - var command = new NpgsqlCommand(sqlQuery, connection); + var command = new NpgsqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); return await Task.FromResult(ReadAsDataSet(reader)); } + + /// + /// Inject parameters into the query to prevent SQL injection. + /// + /// Command to add the parameters to + /// Parameters to add + /// + private NpgsqlCommand AddParameters(NpgsqlCommand command, Dictionary parameters) + { + foreach (var param in parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); + } + return command; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs index f5b57c263..338d9f849 100644 --- a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs +++ b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs @@ -1,5 +1,6 @@ using System.Data; using Elsa.Sql.Client; +using Elsa.Sql.Models; using Microsoft.Data.SqlClient; namespace Elsa.Sql.SqlServer; @@ -9,7 +10,7 @@ public class SqlServerClient : BaseSqlClient, ISqlClient private string? _connectionString; /// - /// Microsoft SQL server client implimentation. + /// Microsoft SQL server client implementation. /// /// public SqlServerClient(string? connectionString) => _connectionString = connectionString; @@ -17,11 +18,12 @@ public class SqlServerClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteCommandAsync(string sqlCommand) + public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqlConnection(_connectionString); connection.Open(); - var command = new SqlCommand(sqlCommand, connection); + var command = new SqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); return result; @@ -30,11 +32,12 @@ public class SqlServerClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteScalarAsync(string sqlQuery) + public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqlConnection(_connectionString); connection.Open(); - var command = new SqlCommand(sqlQuery, connection); + var command = new SqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); return result; @@ -43,13 +46,29 @@ public class SqlServerClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteQueryAsync(string sqlQuery) + public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqlConnection(_connectionString); connection.Open(); - var command = new SqlCommand(sqlQuery, connection); + var command = new SqlCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); return await Task.FromResult(ReadAsDataSet(reader)); } + + /// + /// Inject parameters into the query to prevent SQL injection. + /// + /// Command to add the parameters to + /// Parameters to add + /// + private SqlCommand AddParameters(SqlCommand command, Dictionary parameters) + { + foreach (var param in parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); + } + return command; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs index d67e69342..61caf6ef5 100644 --- a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs +++ b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs @@ -1,5 +1,6 @@ using System.Data; using Elsa.Sql.Client; +using Elsa.Sql.Models; using Microsoft.Data.Sqlite; namespace Elsa.Sql.Sqlite; @@ -9,7 +10,7 @@ public class SqliteClient : BaseSqlClient, ISqlClient private string? _connectionString; /// - /// Sqlite client implimentation. + /// Sqlite client implementation. /// /// public SqliteClient(string? connectionString) => _connectionString = connectionString; @@ -17,11 +18,12 @@ public class SqliteClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteCommandAsync(string sqlCommand) + public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqliteConnection(_connectionString); connection.Open(); - var command = new SqliteCommand(sqlCommand, connection); + var command = new SqliteCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); return result; @@ -30,11 +32,12 @@ public class SqliteClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteScalarAsync(string sqlQuery) + public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqliteConnection(_connectionString); connection.Open(); - var command = new SqliteCommand(sqlQuery, connection); + var command = new SqliteCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); return result; @@ -43,13 +46,29 @@ public class SqliteClient : BaseSqlClient, ISqlClient /// /// /// - public async Task ExecuteQueryAsync(string sqlQuery) + public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) { using var connection = new SqliteConnection(_connectionString); connection.Open(); - var command = new SqliteCommand(sqlQuery, connection); + var command = new SqliteCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); return await Task.FromResult(ReadAsDataSet(reader)); } + + /// + /// Inject parameters into the query to prevent SQL injection. + /// + /// Command to add the parameters to + /// Parameters to add + /// + private SqliteCommand AddParameters(SqliteCommand command, Dictionary parameters) + { + foreach (var param in parameters) + { + command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); + } + return command; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Client/ISqlClient.cs b/src/modules/Elsa.Sql/Client/ISqlClient.cs index 2312097b8..e5b743b3b 100644 --- a/src/modules/Elsa.Sql/Client/ISqlClient.cs +++ b/src/modules/Elsa.Sql/Client/ISqlClient.cs @@ -1,4 +1,5 @@ using System.Data; +using Elsa.Sql.Models; namespace Elsa.Sql.Client; @@ -7,21 +8,21 @@ public interface ISqlClient /// /// Asynchronously executes a Transact-SQL statement against the connection and returns the number of rows affected. /// - /// The command to execute + /// The evaluated query to execute. /// The number of rows affected. - public Task ExecuteCommandAsync(string sqlCommand); + public Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery); /// /// Asynchronously executes the query, and returns the first column of the first row in the result set returned by the query. Additional columns or rows are ignored. /// - /// The query to execute + /// The evaluated query to execute. /// The first column of the first row in the result set, or a null reference if the result set is empty. Returns a maximum of 2033 characters. - public Task ExecuteScalarAsync(string sqlQuery); + public Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery); /// /// Asynchronously executes the query, and returns a dataset of data returned by the query. /// - /// Query to execute + /// The evaluated query to execute. /// DataSet of the queried data - public Task ExecuteQueryAsync(string sqlQuery); + public Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery); } \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Contracts/ISqlEvaluator.cs b/src/modules/Elsa.Sql/Contracts/ISqlEvaluator.cs index 512d9fbd9..0817f409b 100644 --- a/src/modules/Elsa.Sql/Contracts/ISqlEvaluator.cs +++ b/src/modules/Elsa.Sql/Contracts/ISqlEvaluator.cs @@ -1,4 +1,5 @@ using Elsa.Expressions.Models; +using Elsa.Sql.Models; using JetBrains.Annotations; namespace Elsa.Sql.Contracts; @@ -16,8 +17,8 @@ public interface ISqlEvaluator /// The context in which the expression is evaluated. /// A set of options. /// An optional cancellation token. - /// The result of the evaluation. - Task EvaluateAsync( + /// The result. + Task EvaluateAsync( string expression, ExpressionExecutionContext context, ExpressionEvaluatorOptions options, diff --git a/src/modules/Elsa.Sql/Elsa.Sql.csproj b/src/modules/Elsa.Sql/Elsa.Sql.csproj index bfb24be1e..670705651 100644 --- a/src/modules/Elsa.Sql/Elsa.Sql.csproj +++ b/src/modules/Elsa.Sql/Elsa.Sql.csproj @@ -9,6 +9,7 @@ + diff --git a/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs b/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs new file mode 100644 index 000000000..36aae3131 --- /dev/null +++ b/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs @@ -0,0 +1,35 @@ +namespace Elsa.Sql.Models +{ + /// + /// Represents a safely formatted SQL expression result. + /// + public class EvaluatedQuery + { + /// + /// Query with parameterized values + /// + public string Query { get; } + + /// + /// Parameters to inject into the query at execution + /// + public Dictionary Parameters { get; } = new Dictionary(); + + /// + /// An evaluated query response. + /// + /// The evaluated query + public EvaluatedQuery(string query) => Query = query; + + /// + /// An evaluated query response. + /// + /// The evaluated query + /// Parameters to pass into the parameterized query + public EvaluatedQuery(string query, Dictionary parameters) + { + Query = query; + Parameters = parameters; + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs index 88bde44d4..e8a06998d 100644 --- a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs +++ b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs @@ -2,6 +2,7 @@ using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.Sql.Contracts; +using Elsa.Sql.Models; namespace Elsa.Sql.Services; @@ -14,16 +15,18 @@ namespace Elsa.Sql.Services; public class SqlEvaluator() : ISqlEvaluator { /// - public async Task EvaluateAsync( + public async Task EvaluateAsync( string expression, ExpressionExecutionContext context, ExpressionEvaluatorOptions options, CancellationToken cancellationToken = default) { - if (!expression.Contains("@")) return expression; + if (!expression.Contains("@")) return new EvaluatedQuery(expression); var sb = new StringBuilder(); + var parameters = new Dictionary(); int start = 0; + int paramIndex = 0; while (start < expression.Length) { @@ -46,11 +49,14 @@ public class SqlEvaluator() : ISqlEvaluator object? value = ResolveValue(key, context); if (value is null) throw new NullReferenceException($"No value found for '{key}'."); - sb.Append(value?.ToString() ?? $"<{key}>"); + string paramName = $"@param{paramIndex++}"; + parameters[paramName] = value; + + sb.Append(paramName); start = endIndex; } - return sb.ToString(); + return new EvaluatedQuery(sb.ToString(), parameters); } private object? ResolveValue(string key, ExpressionExecutionContext context) From d8b6eda93ff228100b6d85feda2962f5c9b4f986 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 15 Mar 2025 01:51:42 +0000 Subject: [PATCH 2/6] Simplify SQL client implementations to reduce repetitive code. --- src/modules/Elsa.Sql.MySql/MySqlClient.cs | 74 ++------------- .../Elsa.Sql.PostgreSql/PostgreSqlClient.cs | 74 ++------------- .../Elsa.Sql.SqlServer/SqlServerClient.cs | 74 ++------------- src/modules/Elsa.Sql.Sqlite/SqliteClient.cs | 74 ++------------- src/modules/Elsa.Sql/Client/BaseSqlClient.cs | 94 ++++++++++++++++++- 5 files changed, 123 insertions(+), 267 deletions(-) diff --git a/src/modules/Elsa.Sql.MySql/MySqlClient.cs b/src/modules/Elsa.Sql.MySql/MySqlClient.cs index f8a17c61e..60669f7d1 100644 --- a/src/modules/Elsa.Sql.MySql/MySqlClient.cs +++ b/src/modules/Elsa.Sql.MySql/MySqlClient.cs @@ -1,74 +1,16 @@ using MySql.Data.MySqlClient; using Elsa.Sql.Client; -using System.Data; -using Elsa.Sql.Models; +using System.Data.Common; namespace Elsa.Sql.MySql; -public class MySqlClient : BaseSqlClient, ISqlClient +/// +/// MySql client implementation. +/// +/// +public class MySqlClient(string connectionString) : BaseSqlClient(connectionString) { - private string? _connectionString; + protected override DbConnection CreateConnection() => new MySqlConnection(_connectionString); - /// - /// MySql client implementation. - /// - /// - public MySqlClient(string? connectionString) => _connectionString = connectionString; - - /// - /// - /// - public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new MySqlConnection(_connectionString); - connection.Open(); - var command = new MySqlCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - var result = await command.ExecuteNonQueryAsync(); - return result; - } - - /// - /// - /// - public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new MySqlConnection(_connectionString); - connection.Open(); - var command = new MySqlCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - var result = await command.ExecuteScalarAsync(); - return result; - } - - /// - /// - /// - public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new MySqlConnection(_connectionString); - connection.Open(); - var command = new MySqlCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - using var reader = await command.ExecuteReaderAsync(); - return await Task.FromResult(ReadAsDataSet(reader)); - } - - /// - /// Inject parameters into the query to prevent SQL injection. - /// - /// Command to add the parameters to - /// Parameters to add - /// - private MySqlCommand AddParameters(MySqlCommand command, Dictionary parameters) - { - foreach (var param in parameters) - { - command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); - } - return command; - } + protected override DbCommand CreateCommand(string query, DbConnection connection) => new MySqlCommand(query, (MySqlConnection)connection); } \ No newline at end of file diff --git a/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs index 48d54101a..5eb5982ba 100644 --- a/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs +++ b/src/modules/Elsa.Sql.PostgreSql/PostgreSqlClient.cs @@ -1,74 +1,16 @@ using Npgsql; using Elsa.Sql.Client; -using System.Data; -using Elsa.Sql.Models; +using System.Data.Common; namespace Elsa.Sql.PostgreSql; -public class PostgreSqlClient : BaseSqlClient, ISqlClient +/// +/// PostgreSQL client implementation. +/// +/// +public class PostgreSqlClient(string connectionString) : BaseSqlClient(connectionString) { - private string? _connectionString; + protected override DbConnection CreateConnection() => new NpgsqlConnection(_connectionString); - /// - /// PostgreSQL client implementation. - /// - /// - public PostgreSqlClient(string? connectionString) => _connectionString = connectionString; - - /// - /// - /// - public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new NpgsqlConnection(_connectionString); - connection.Open(); - var command = new NpgsqlCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - var result = await command.ExecuteNonQueryAsync(); - return result; - } - - /// - /// - /// - public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new NpgsqlConnection(_connectionString); - connection.Open(); - var command = new NpgsqlCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - var result = await command.ExecuteScalarAsync(); - return result; - } - - /// - /// - /// - public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new NpgsqlConnection(_connectionString); - connection.Open(); - var command = new NpgsqlCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - using var reader = await command.ExecuteReaderAsync(); - return await Task.FromResult(ReadAsDataSet(reader)); - } - - /// - /// Inject parameters into the query to prevent SQL injection. - /// - /// Command to add the parameters to - /// Parameters to add - /// - private NpgsqlCommand AddParameters(NpgsqlCommand command, Dictionary parameters) - { - foreach (var param in parameters) - { - command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); - } - return command; - } + protected override DbCommand CreateCommand(string query, DbConnection connection) => new NpgsqlCommand(query, (NpgsqlConnection)connection); } \ No newline at end of file diff --git a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs index 338d9f849..e8780c359 100644 --- a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs +++ b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs @@ -1,74 +1,16 @@ -using System.Data; +using System.Data.Common; using Elsa.Sql.Client; -using Elsa.Sql.Models; using Microsoft.Data.SqlClient; namespace Elsa.Sql.SqlServer; -public class SqlServerClient : BaseSqlClient, ISqlClient +/// +/// Microsoft SQL server client implementation. +/// +/// +public class SqlServerClient(string connectionString) : BaseSqlClient(connectionString) { - private string? _connectionString; + protected override DbConnection CreateConnection() => new SqlConnection(_connectionString); - /// - /// Microsoft SQL server client implementation. - /// - /// - public SqlServerClient(string? connectionString) => _connectionString = connectionString; - - /// - /// - /// - public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new SqlConnection(_connectionString); - connection.Open(); - var command = new SqlCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - var result = await command.ExecuteNonQueryAsync(); - return result; - } - - /// - /// - /// - public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new SqlConnection(_connectionString); - connection.Open(); - var command = new SqlCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - var result = await command.ExecuteScalarAsync(); - return result; - } - - /// - /// - /// - public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new SqlConnection(_connectionString); - connection.Open(); - var command = new SqlCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - using var reader = await command.ExecuteReaderAsync(); - return await Task.FromResult(ReadAsDataSet(reader)); - } - - /// - /// Inject parameters into the query to prevent SQL injection. - /// - /// Command to add the parameters to - /// Parameters to add - /// - private SqlCommand AddParameters(SqlCommand command, Dictionary parameters) - { - foreach (var param in parameters) - { - command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); - } - return command; - } + protected override DbCommand CreateCommand(string query, DbConnection connection) => new SqlCommand(query, (SqlConnection)connection); } \ No newline at end of file diff --git a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs index 61caf6ef5..ec43f5245 100644 --- a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs +++ b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs @@ -1,74 +1,16 @@ -using System.Data; +using System.Data.Common; using Elsa.Sql.Client; -using Elsa.Sql.Models; using Microsoft.Data.Sqlite; namespace Elsa.Sql.Sqlite; -public class SqliteClient : BaseSqlClient, ISqlClient +/// +/// Sqlite client implementation. +/// +/// +public class SqliteClient(string connectionString) : BaseSqlClient(connectionString) { - private string? _connectionString; + protected override DbConnection CreateConnection() => new SqliteConnection(_connectionString); - /// - /// Sqlite client implementation. - /// - /// - public SqliteClient(string? connectionString) => _connectionString = connectionString; - - /// - /// - /// - public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new SqliteConnection(_connectionString); - connection.Open(); - var command = new SqliteCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - var result = await command.ExecuteNonQueryAsync(); - return result; - } - - /// - /// - /// - public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new SqliteConnection(_connectionString); - connection.Open(); - var command = new SqliteCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - var result = await command.ExecuteScalarAsync(); - return result; - } - - /// - /// - /// - public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) - { - using var connection = new SqliteConnection(_connectionString); - connection.Open(); - var command = new SqliteCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); - - using var reader = await command.ExecuteReaderAsync(); - return await Task.FromResult(ReadAsDataSet(reader)); - } - - /// - /// Inject parameters into the query to prevent SQL injection. - /// - /// Command to add the parameters to - /// Parameters to add - /// - private SqliteCommand AddParameters(SqliteCommand command, Dictionary parameters) - { - foreach (var param in parameters) - { - command.Parameters.AddWithValue(param.Key, param.Value ?? DBNull.Value); - } - return command; - } + protected override DbCommand CreateCommand(string query, DbConnection connection) => new SqliteCommand(query, (SqliteConnection)connection); } \ No newline at end of file diff --git a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs index 6ed7dbfd9..872d4cfcc 100644 --- a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs +++ b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs @@ -1,15 +1,103 @@ using System.Data; +using System.Data.Common; +using Elsa.Sql.Models; namespace Elsa.Sql.Client; -public abstract class BaseSqlClient +public abstract class BaseSqlClient : ISqlClient { + /// + /// The connection string used to connect with the database. + /// + protected readonly string _connectionString; + + /// + /// Create a connection using the client specific connection. + /// + /// + protected abstract DbConnection CreateConnection(); + + /// + /// Create a command using the client specific connection. + /// + /// + /// + /// + protected abstract DbCommand CreateCommand(string query, DbConnection connection); + + /// + /// + /// + /// + protected BaseSqlClient(string connectionString) => _connectionString = connectionString; + + /// + /// + /// + public async Task ExecuteCommandAsync(EvaluatedQuery evaluatedQuery) + { + using var connection = CreateConnection(); + connection.Open(); + var command = CreateCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); + + var result = await command.ExecuteNonQueryAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteScalarAsync(EvaluatedQuery evaluatedQuery) + { + using var connection = CreateConnection(); + connection.Open(); + var command = CreateCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); + + var result = await command.ExecuteScalarAsync(); + return result; + } + + /// + /// + /// + public async Task ExecuteQueryAsync(EvaluatedQuery evaluatedQuery) + { + using var connection = CreateConnection(); + connection.Open(); + var command = CreateCommand(evaluatedQuery.Query, connection); + AddParameters(command, evaluatedQuery.Parameters); + + using var reader = await command.ExecuteReaderAsync(); + return await Task.FromResult(ReadAsDataSet(reader)); + } + + /// + /// Inject parameters into the query to prevent SQL injection. + /// + /// Command to add the parameters to + /// Parameters to add + /// + private DbCommand AddParameters(DbCommand command, Dictionary parameters) + { + // Add parameters dynamically + foreach (var param in parameters) + { + var dbParam = command.CreateParameter(); + dbParam.ParameterName = param.Key; + dbParam.Value = param.Value ?? DBNull.Value; + command.Parameters.Add(dbParam); + } + return command; + } + /// /// Returns data as a . /// /// Reader to return data from. /// of data. - protected static DataSet ReadAsDataSet(IDataReader reader) + private DataSet ReadAsDataSet(IDataReader reader) { var dataSet = new DataSet("dataset"); dataSet.Tables.Add(ReadAsDataTable(reader)); @@ -21,7 +109,7 @@ public abstract class BaseSqlClient /// /// Reader to return data from. /// of data. - protected static DataTable ReadAsDataTable(IDataReader reader) + private DataTable ReadAsDataTable(IDataReader reader) { var data = new DataTable(); var schemaTable =reader.GetSchemaTable(); From d3dc6f35976c9700d62cda8ce53c3eb4609f30e8 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 15 Mar 2025 22:06:08 +0000 Subject: [PATCH 3/6] Updated the SqlEvaluator to work with {{ }} rather than @ for expression. Supporting updates also added. --- src/modules/Elsa.Sql.MySql/MySqlClient.cs | 6 ++ .../Elsa.Sql.SqlServer/SqlServerClient.cs | 2 + src/modules/Elsa.Sql.Sqlite/SqliteClient.cs | 2 + src/modules/Elsa.Sql/Client/BaseSqlClient.cs | 57 ++++++++++++++++--- src/modules/Elsa.Sql/Elsa.Sql.csproj | 1 - src/modules/Elsa.Sql/Models/EvaluatedQuery.cs | 4 +- src/modules/Elsa.Sql/Services/SqlEvaluator.cs | 34 ++++++----- 7 files changed, 81 insertions(+), 25 deletions(-) diff --git a/src/modules/Elsa.Sql.MySql/MySqlClient.cs b/src/modules/Elsa.Sql.MySql/MySqlClient.cs index 60669f7d1..425f701de 100644 --- a/src/modules/Elsa.Sql.MySql/MySqlClient.cs +++ b/src/modules/Elsa.Sql.MySql/MySqlClient.cs @@ -10,6 +10,12 @@ namespace Elsa.Sql.MySql; /// public class MySqlClient(string connectionString) : BaseSqlClient(connectionString) { + public override string ParameterMarker { get; set; } = "@"; + + public override string ParameterText { get; set; } = ""; + + public override bool IncrementParameter { get; set; } = false; + protected override DbConnection CreateConnection() => new MySqlConnection(_connectionString); protected override DbCommand CreateCommand(string query, DbConnection connection) => new MySqlCommand(query, (MySqlConnection)connection); diff --git a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs index e8780c359..96efacb63 100644 --- a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs +++ b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs @@ -10,6 +10,8 @@ namespace Elsa.Sql.SqlServer; /// public class SqlServerClient(string connectionString) : BaseSqlClient(connectionString) { + public override string ParameterText { get; set; } = "p"; + protected override DbConnection CreateConnection() => new SqlConnection(_connectionString); protected override DbCommand CreateCommand(string query, DbConnection connection) => new SqlCommand(query, (SqlConnection)connection); diff --git a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs index ec43f5245..e6ad5afcb 100644 --- a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs +++ b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs @@ -10,6 +10,8 @@ namespace Elsa.Sql.Sqlite; /// public class SqliteClient(string connectionString) : BaseSqlClient(connectionString) { + public override string ParameterText { get; set; } = "p"; + protected override DbConnection CreateConnection() => new SqliteConnection(_connectionString); protected override DbCommand CreateCommand(string query, DbConnection connection) => new SqliteCommand(query, (SqliteConnection)connection); diff --git a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs index 872d4cfcc..f8714bd26 100644 --- a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs +++ b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs @@ -1,5 +1,6 @@ using System.Data; using System.Data.Common; +using System.Text; using Elsa.Sql.Models; namespace Elsa.Sql.Client; @@ -11,6 +12,24 @@ public abstract class BaseSqlClient : ISqlClient /// protected readonly string _connectionString; + /// + /// The marker used when injecting parameters into a query. + /// Default: "@" + /// + public virtual string ParameterMarker { get; set; } = "@"; + + /// + /// The text following the ParameterMarkerwhen injecting parameters into a query + /// Default: + /// + public virtual string ParameterText { get; set; } = string.Empty; + + /// + /// Set to true to add a counter to the end of the parameter string + /// Default: false + /// + public virtual bool IncrementParameter { get; set; } = true; + /// /// Create a connection using the client specific connection. /// @@ -38,8 +57,9 @@ public abstract class BaseSqlClient : ISqlClient { using var connection = CreateConnection(); connection.Open(); - var command = CreateCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); + var query = ReplaceQueryParameters(evaluatedQuery); + var command = CreateCommand(query, connection); + AddCommandParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); return result; @@ -52,8 +72,9 @@ public abstract class BaseSqlClient : ISqlClient { using var connection = CreateConnection(); connection.Open(); - var command = CreateCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); + var query = ReplaceQueryParameters(evaluatedQuery); + var command = CreateCommand(query, connection); + AddCommandParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); return result; @@ -66,20 +87,42 @@ public abstract class BaseSqlClient : ISqlClient { using var connection = CreateConnection(); connection.Open(); - var command = CreateCommand(evaluatedQuery.Query, connection); - AddParameters(command, evaluatedQuery.Parameters); + var query = ReplaceQueryParameters(evaluatedQuery); + var command = CreateCommand(query, connection); + AddCommandParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); return await Task.FromResult(ReadAsDataSet(reader)); } + /// + /// Replace the evaluated parameters with client specific parameters. + /// + /// Query to replace parameters for. + /// + private string ReplaceQueryParameters(EvaluatedQuery evaluatedQuery) + { + var count = 1; + var clientUpdatedParams = new Dictionary(); + var queryBuilder = new StringBuilder(evaluatedQuery.Query); + foreach (var param in evaluatedQuery.Parameters) + { + var counterValue = IncrementParameter ? count++.ToString() : string.Empty; + var newKey = $"{ParameterMarker}{ParameterText}{counterValue}"; + queryBuilder.Replace(param.Key, newKey); + clientUpdatedParams[newKey] = param.Value; + } + evaluatedQuery.Parameters = clientUpdatedParams; + return queryBuilder.ToString(); + } + /// /// Inject parameters into the query to prevent SQL injection. /// /// Command to add the parameters to /// Parameters to add /// - private DbCommand AddParameters(DbCommand command, Dictionary parameters) + private DbCommand AddCommandParameters(DbCommand command, Dictionary parameters) { // Add parameters dynamically foreach (var param in parameters) diff --git a/src/modules/Elsa.Sql/Elsa.Sql.csproj b/src/modules/Elsa.Sql/Elsa.Sql.csproj index 670705651..bfb24be1e 100644 --- a/src/modules/Elsa.Sql/Elsa.Sql.csproj +++ b/src/modules/Elsa.Sql/Elsa.Sql.csproj @@ -9,7 +9,6 @@ - diff --git a/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs b/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs index 36aae3131..1570fab0e 100644 --- a/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs +++ b/src/modules/Elsa.Sql/Models/EvaluatedQuery.cs @@ -8,12 +8,12 @@ /// /// Query with parameterized values /// - public string Query { get; } + public string Query { get; set; } /// /// Parameters to inject into the query at execution /// - public Dictionary Parameters { get; } = new Dictionary(); + public Dictionary Parameters { get; set; } = new Dictionary(); /// /// An evaluated query response. diff --git a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs index e8a06998d..7bfdc42ea 100644 --- a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs +++ b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs @@ -21,39 +21,43 @@ public class SqlEvaluator() : ISqlEvaluator ExpressionEvaluatorOptions options, CancellationToken cancellationToken = default) { - if (!expression.Contains("@")) return new EvaluatedQuery(expression); + if (!expression.Contains("{{")) return new EvaluatedQuery(expression); var sb = new StringBuilder(); - var parameters = new Dictionary(); int start = 0; + var parameters = new Dictionary(); int paramIndex = 0; while (start < expression.Length) { - int atIndex = expression.IndexOf('@', start); - if (atIndex == -1) + int openIndex = expression.IndexOf("{{", start); + if (openIndex == -1) { - sb.Append(expression.Substring(start)); + sb.Append(expression.AsSpan(start)); break; } - sb.Append(expression.Substring(start, atIndex - start)); + // Append everything before {{ + sb.Append(expression.AsSpan(start, openIndex - start)); - int endIndex = atIndex + 1; - while (endIndex < expression.Length && (char.IsLetterOrDigit((char)expression[endIndex]) || expression[endIndex] == '.' || expression[endIndex] == '_')) - { - endIndex++; - } + // Find the closing }} + int closeIndex = expression.IndexOf("}}", openIndex + 2); + if (closeIndex == -1) throw new FormatException("Unmatched '{{' found in SQL expression."); - string key = expression.Substring(atIndex + 1, endIndex - atIndex - 1); + // Extract key + string key = expression.Substring(openIndex + 2, closeIndex - openIndex - 2).Trim(); + if (string.IsNullOrEmpty(key)) throw new FormatException("Empty placeholder '{{}}' is not allowed."); + + // Resolve value object? value = ResolveValue(key, context); - if (value is null) throw new NullReferenceException($"No value found for '{key}'."); + if (value is null) throw new NullReferenceException($"No value found for '{{{{{key}}}}}'."); - string paramName = $"@param{paramIndex++}"; + // Replace with parameterized name + string paramName = $"{{{{p{paramIndex++}}}}}"; parameters[paramName] = value; sb.Append(paramName); - start = endIndex; + start = closeIndex + 2; } return new EvaluatedQuery(sb.ToString(), parameters); From 24da6ab288dc6cd5bdfe07bf6c98d08de940788e Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 16 Mar 2025 00:44:35 +0000 Subject: [PATCH 4/6] Simplify SqlEvaluator implementation and tidy code. --- src/modules/Elsa.Sql.MySql/MySqlClient.cs | 6 --- .../Elsa.Sql.SqlServer/SqlServerClient.cs | 2 - src/modules/Elsa.Sql.Sqlite/SqliteClient.cs | 2 - src/modules/Elsa.Sql/Client/BaseSqlClient.cs | 43 ++++--------------- src/modules/Elsa.Sql/Client/ISqlClient.cs | 15 +++++++ src/modules/Elsa.Sql/Services/SqlEvaluator.cs | 40 +++++++++++------ 6 files changed, 51 insertions(+), 57 deletions(-) diff --git a/src/modules/Elsa.Sql.MySql/MySqlClient.cs b/src/modules/Elsa.Sql.MySql/MySqlClient.cs index 425f701de..60669f7d1 100644 --- a/src/modules/Elsa.Sql.MySql/MySqlClient.cs +++ b/src/modules/Elsa.Sql.MySql/MySqlClient.cs @@ -10,12 +10,6 @@ namespace Elsa.Sql.MySql; /// public class MySqlClient(string connectionString) : BaseSqlClient(connectionString) { - public override string ParameterMarker { get; set; } = "@"; - - public override string ParameterText { get; set; } = ""; - - public override bool IncrementParameter { get; set; } = false; - protected override DbConnection CreateConnection() => new MySqlConnection(_connectionString); protected override DbCommand CreateCommand(string query, DbConnection connection) => new MySqlCommand(query, (MySqlConnection)connection); diff --git a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs index 96efacb63..e8780c359 100644 --- a/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs +++ b/src/modules/Elsa.Sql.SqlServer/SqlServerClient.cs @@ -10,8 +10,6 @@ namespace Elsa.Sql.SqlServer; /// public class SqlServerClient(string connectionString) : BaseSqlClient(connectionString) { - public override string ParameterText { get; set; } = "p"; - protected override DbConnection CreateConnection() => new SqlConnection(_connectionString); protected override DbCommand CreateCommand(string query, DbConnection connection) => new SqlCommand(query, (SqlConnection)connection); diff --git a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs index e6ad5afcb..ec43f5245 100644 --- a/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs +++ b/src/modules/Elsa.Sql.Sqlite/SqliteClient.cs @@ -10,8 +10,6 @@ namespace Elsa.Sql.Sqlite; /// public class SqliteClient(string connectionString) : BaseSqlClient(connectionString) { - public override string ParameterText { get; set; } = "p"; - protected override DbConnection CreateConnection() => new SqliteConnection(_connectionString); protected override DbCommand CreateCommand(string query, DbConnection connection) => new SqliteCommand(query, (SqliteConnection)connection); diff --git a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs index f8714bd26..314f103ba 100644 --- a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs +++ b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs @@ -1,6 +1,5 @@ using System.Data; using System.Data.Common; -using System.Text; using Elsa.Sql.Models; namespace Elsa.Sql.Client; @@ -19,14 +18,14 @@ public abstract class BaseSqlClient : ISqlClient public virtual string ParameterMarker { get; set; } = "@"; /// - /// The text following the ParameterMarkerwhen injecting parameters into a query - /// Default: + /// The text following the ParameterMarker when injecting parameters into a query. + /// Default: "param" /// - public virtual string ParameterText { get; set; } = string.Empty; + public virtual string ParameterText { get; set; } = "p"; /// - /// Set to true to add a counter to the end of the parameter string - /// Default: false + /// Set to true to add a counter to the end of the parameter string. + /// Default: true /// public virtual bool IncrementParameter { get; set; } = true; @@ -57,8 +56,7 @@ public abstract class BaseSqlClient : ISqlClient { using var connection = CreateConnection(); connection.Open(); - var query = ReplaceQueryParameters(evaluatedQuery); - var command = CreateCommand(query, connection); + var command = CreateCommand(evaluatedQuery.Query, connection); AddCommandParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteNonQueryAsync(); @@ -72,8 +70,7 @@ public abstract class BaseSqlClient : ISqlClient { using var connection = CreateConnection(); connection.Open(); - var query = ReplaceQueryParameters(evaluatedQuery); - var command = CreateCommand(query, connection); + var command = CreateCommand(evaluatedQuery.Query, connection); AddCommandParameters(command, evaluatedQuery.Parameters); var result = await command.ExecuteScalarAsync(); @@ -87,8 +84,7 @@ public abstract class BaseSqlClient : ISqlClient { using var connection = CreateConnection(); connection.Open(); - var query = ReplaceQueryParameters(evaluatedQuery); - var command = CreateCommand(query, connection); + var command = CreateCommand(evaluatedQuery.Query, connection); AddCommandParameters(command, evaluatedQuery.Parameters); using var reader = await command.ExecuteReaderAsync(); @@ -96,28 +92,7 @@ public abstract class BaseSqlClient : ISqlClient } /// - /// Replace the evaluated parameters with client specific parameters. - /// - /// Query to replace parameters for. - /// - private string ReplaceQueryParameters(EvaluatedQuery evaluatedQuery) - { - var count = 1; - var clientUpdatedParams = new Dictionary(); - var queryBuilder = new StringBuilder(evaluatedQuery.Query); - foreach (var param in evaluatedQuery.Parameters) - { - var counterValue = IncrementParameter ? count++.ToString() : string.Empty; - var newKey = $"{ParameterMarker}{ParameterText}{counterValue}"; - queryBuilder.Replace(param.Key, newKey); - clientUpdatedParams[newKey] = param.Value; - } - evaluatedQuery.Parameters = clientUpdatedParams; - return queryBuilder.ToString(); - } - - /// - /// Inject parameters into the query to prevent SQL injection. + /// Add parameters into the query to prevent SQL injection. /// /// Command to add the parameters to /// Parameters to add diff --git a/src/modules/Elsa.Sql/Client/ISqlClient.cs b/src/modules/Elsa.Sql/Client/ISqlClient.cs index e5b743b3b..2cc3eb5b9 100644 --- a/src/modules/Elsa.Sql/Client/ISqlClient.cs +++ b/src/modules/Elsa.Sql/Client/ISqlClient.cs @@ -5,6 +5,21 @@ namespace Elsa.Sql.Client; public interface ISqlClient { + /// + /// The marker used when injecting parameters into a query. + /// + public string ParameterMarker { get; set; } + + /// + /// The text following the ParameterMarker when injecting parameters into a query. + /// + public string ParameterText { get; set; } + + /// + /// Set to true to add a counter to the end of the parameter string. + /// + public bool IncrementParameter { get; set; } + /// /// Asynchronously executes a Transact-SQL statement against the connection and returns the number of rows affected. /// diff --git a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs index 7bfdc42ea..c0947bc2e 100644 --- a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs +++ b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs @@ -3,6 +3,7 @@ using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.Sql.Contracts; using Elsa.Sql.Models; +using Elsa.Workflows; namespace Elsa.Sql.Services; @@ -14,6 +15,10 @@ namespace Elsa.Sql.Services; /// public class SqlEvaluator() : ISqlEvaluator { + private WorkflowExecutionContext executionContext; + private ActivityExecutionContext activityContext; + private ExpressionExecutionContext expressionContext; + /// public async Task EvaluateAsync( string expression, @@ -23,6 +28,14 @@ public class SqlEvaluator() : ISqlEvaluator { if (!expression.Contains("{{")) return new EvaluatedQuery(expression); + expressionContext = context; + executionContext = context.GetWorkflowExecutionContext(); + activityContext = context.GetActivityExecutionContext(); + + // Create client + var factory = context.GetRequiredService(); + var client = factory.CreateClient(activityContext.ActivityState["Client"].ToString(), activityContext.ActivityState["ConnectionString"].ToString()); + var sb = new StringBuilder(); int start = 0; var parameters = new Dictionary(); @@ -49,11 +62,12 @@ public class SqlEvaluator() : ISqlEvaluator if (string.IsNullOrEmpty(key)) throw new FormatException("Empty placeholder '{{}}' is not allowed."); // Resolve value - object? value = ResolveValue(key, context); - if (value is null) throw new NullReferenceException($"No value found for '{{{{{key}}}}}'."); + object? value = ResolveValue(key); + if (value is null) throw new NullReferenceException($"No value found for {{{{{key}}}}}."); // Replace with parameterized name - string paramName = $"{{{{p{paramIndex++}}}}}"; + var counterValue = client.IncrementParameter ? paramIndex++.ToString() : string.Empty; + string paramName = $"{client.ParameterMarker}{client.ParameterText}{counterValue}"; parameters[paramName] = value; sb.Append(paramName); @@ -63,19 +77,19 @@ public class SqlEvaluator() : ISqlEvaluator return new EvaluatedQuery(sb.ToString(), parameters); } - private object? ResolveValue(string key, ExpressionExecutionContext context) + private object? ResolveValue(string key) { return key switch { - "Workflow.Definition.Id" => context.GetWorkflowExecutionContext().Workflow.Identity.DefinitionId, - "Workflow.Definition.Version.Id" => context.GetWorkflowExecutionContext().Workflow.Identity.Id, - "Workflow.Definition.Version" => context.GetWorkflowExecutionContext().Workflow.Identity.Version, - "Workflow.Instance.Id" => context.GetActivityExecutionContext().WorkflowExecutionContext.Id, - "Correlation.Id" => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId, - "LastResult" => context.GetLastResult(), - var i when i.StartsWith("Input.") => context.GetWorkflowExecutionContext().Input.TryGetValue(i.Substring(6), out var v) ? v : null, - var o when o.StartsWith("Output.") => context.GetWorkflowExecutionContext().Output.TryGetValue(o.Substring(7), out var v) ? v : null, - var v when v.StartsWith("Variables.") => context.GetWorkflowExecutionContext().Variables.FirstOrDefault(x => x.Name == v.Substring(10), null)?.Value ?? null, + "Workflow.Definition.Id" => executionContext.Workflow.Identity.DefinitionId, + "Workflow.Definition.Version.Id" => executionContext.Workflow.Identity.Id, + "Workflow.Definition.Version" => executionContext.Workflow.Identity.Version, + "Workflow.Instance.Id" => activityContext.WorkflowExecutionContext.Id, + "Correlation.Id" => activityContext.WorkflowExecutionContext.CorrelationId, + "LastResult" => expressionContext.GetLastResult(), + var i when i.StartsWith("Input.") => executionContext.Input.TryGetValue(i.Substring(6), out var v) ? v : null, + var o when o.StartsWith("Output.") => executionContext.Output.TryGetValue(o.Substring(7), out var v) ? v : null, + var v when v.StartsWith("Variables.") => executionContext.Variables.FirstOrDefault(x => x.Name == v.Substring(10), null)?.Value ?? null, _ => null }; } From 5f43966233ea8b61e853fcb19bd3937defac56e8 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 16 Mar 2025 00:59:53 +0000 Subject: [PATCH 5/6] BaseSqlClient code tidy. --- src/modules/Elsa.Sql/Client/BaseSqlClient.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs index 314f103ba..9f7cca113 100644 --- a/src/modules/Elsa.Sql/Client/BaseSqlClient.cs +++ b/src/modules/Elsa.Sql/Client/BaseSqlClient.cs @@ -29,6 +29,12 @@ public abstract class BaseSqlClient : ISqlClient /// public virtual bool IncrementParameter { get; set; } = true; + /// + /// Default base implementation for an SQL client. + /// + /// + protected BaseSqlClient(string connectionString) => _connectionString = connectionString; + /// /// Create a connection using the client specific connection. /// @@ -43,12 +49,6 @@ public abstract class BaseSqlClient : ISqlClient /// protected abstract DbCommand CreateCommand(string query, DbConnection connection); - /// - /// - /// - /// - protected BaseSqlClient(string connectionString) => _connectionString = connectionString; - /// /// /// @@ -99,7 +99,6 @@ public abstract class BaseSqlClient : ISqlClient /// private DbCommand AddCommandParameters(DbCommand command, Dictionary parameters) { - // Add parameters dynamically foreach (var param in parameters) { var dbParam = command.CreateParameter(); From 95b67ea0022f8217a16fc76f33fa6b690a2a0e0d Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 16 Mar 2025 16:42:32 +0000 Subject: [PATCH 6/6] Updates error handling to allow null values to be passed to the query. Errors are now thrown for when no matching properties are found. --- src/modules/Elsa.Sql/Services/SqlEvaluator.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs index c0947bc2e..10ee5a0df 100644 --- a/src/modules/Elsa.Sql/Services/SqlEvaluator.cs +++ b/src/modules/Elsa.Sql/Services/SqlEvaluator.cs @@ -61,14 +61,10 @@ public class SqlEvaluator() : ISqlEvaluator string key = expression.Substring(openIndex + 2, closeIndex - openIndex - 2).Trim(); if (string.IsNullOrEmpty(key)) throw new FormatException("Empty placeholder '{{}}' is not allowed."); - // Resolve value - object? value = ResolveValue(key); - if (value is null) throw new NullReferenceException($"No value found for {{{{{key}}}}}."); - - // Replace with parameterized name + // Resolve value and replace with parameterized name var counterValue = client.IncrementParameter ? paramIndex++.ToString() : string.Empty; string paramName = $"{client.ParameterMarker}{client.ParameterText}{counterValue}"; - parameters[paramName] = value; + parameters[paramName] = ResolveValue(key); sb.Append(paramName); start = closeIndex + 2; @@ -90,7 +86,7 @@ public class SqlEvaluator() : ISqlEvaluator var i when i.StartsWith("Input.") => executionContext.Input.TryGetValue(i.Substring(6), out var v) ? v : null, var o when o.StartsWith("Output.") => executionContext.Output.TryGetValue(o.Substring(7), out var v) ? v : null, var v when v.StartsWith("Variables.") => executionContext.Variables.FirstOrDefault(x => x.Name == v.Substring(10), null)?.Value ?? null, - _ => null + _ => throw new NullReferenceException($"No matching property found for {{{{{key}}}}}.") }; } } \ No newline at end of file