Updated the SqlEvaluator to work with {{ }} rather than @ for expression. Supporting updates also added.

This commit is contained in:
Matt 2025-03-15 22:06:08 +00:00
parent d8b6eda93f
commit d3dc6f3597
7 changed files with 81 additions and 25 deletions

View file

@ -10,6 +10,12 @@ namespace Elsa.Sql.MySql;
/// <param name="connectionString"></param>
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);

View file

@ -10,6 +10,8 @@ namespace Elsa.Sql.SqlServer;
/// <param name="connectionString"></param>
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);

View file

@ -10,6 +10,8 @@ namespace Elsa.Sql.Sqlite;
/// <param name="connectionString"></param>
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);

View file

@ -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
/// </summary>
protected readonly string _connectionString;
/// <summary>
/// The marker used when injecting parameters into a query.
/// Default: "@"
/// </summary>
public virtual string ParameterMarker { get; set; } = "@";
/// <summary>
/// The text following the <c>ParameterMarker</c>when injecting parameters into a query
/// Default: <see cref="string.Empty"/>
/// </summary>
public virtual string ParameterText { get; set; } = string.Empty;
/// <summary>
/// Set to true to add a counter to the end of the parameter string
/// Default: false
/// </summary>
public virtual bool IncrementParameter { get; set; } = true;
/// <summary>
/// Create a connection using the client specific connection.
/// </summary>
@ -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));
}
/// <summary>
/// Replace the evaluated parameters with client specific parameters.
/// </summary>
/// <param name="evaluatedQuery">Query to replace parameters for.</param>
/// <returns></returns>
private string ReplaceQueryParameters(EvaluatedQuery evaluatedQuery)
{
var count = 1;
var clientUpdatedParams = new Dictionary<string, object>();
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();
}
/// <summary>
/// Inject parameters into the query to prevent SQL injection.
/// </summary>
/// <param name="command">Command to add the parameters to</param>
/// <param name="parameters">Parameters to add</param>
/// <returns></returns>
private DbCommand AddParameters(DbCommand command, Dictionary<string, object?> parameters)
private DbCommand AddCommandParameters(DbCommand command, Dictionary<string, object?> parameters)
{
// Add parameters dynamically
foreach (var param in parameters)

View file

@ -9,7 +9,6 @@
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
<ProjectReference Include="..\Elsa.Sql.Models\Elsa.Sql.Models.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>

View file

@ -8,12 +8,12 @@
/// <summary>
/// Query with parameterized values
/// </summary>
public string Query { get; }
public string Query { get; set; }
/// <summary>
/// Parameters to inject into the query at execution
/// </summary>
public Dictionary<string, object?> Parameters { get; } = new Dictionary<string, object?>();
public Dictionary<string, object?> Parameters { get; set; } = new Dictionary<string, object?>();
/// <summary>
/// An evaluated query response.

View file

@ -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<string, object?>();
int start = 0;
var parameters = new Dictionary<string, object?>();
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);