Add project files.

This commit is contained in:
2025-08-18 17:11:43 +03:30
parent 807dc263be
commit e5eb10d604
3943 changed files with 223719 additions and 0 deletions
@@ -0,0 +1,10 @@
using System.Reflection;
using Elsa.Workflows.UIHints.CodeEditor;
// ReSharper disable once CheckNamespace
namespace Elsa.Sql.Activities;
internal class SqlCodeOptionsProvider : CodeEditorOptionsProviderBase
{
protected override string GetLanguage(PropertyInfo propertyInfo, object? context) => "sql";
}
@@ -0,0 +1,88 @@
using System.Runtime.CompilerServices;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Sql.Contracts;
using Elsa.Sql.UIHints;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Elsa.Workflows.UIHints;
namespace Elsa.Sql.Activities;
/// <summary>
/// Execute given SQL command and returns the number of rows affected.
/// </summary>
[Activity("Elsa", "SQL", "Execute given SQL command and returns the number of rows affected.", DisplayName = "SQL Command", Kind = ActivityKind.Task)]
public class SqlCommand : Activity
{
/// <summary>
/// <inheritdoc/>
/// </summary>
public SqlCommand([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <summary>
/// Database client to connect with.
/// </summary>
[Input(
Description = "Database client.",
UIHint = InputUIHints.DropDown,
UIHandler = typeof(SqlClientsDropDownProvider))]
public Input<string?> Client { get; set; } = null!;
/// <summary>
/// Connection string.
/// </summary>
[Input(
Description = "Connection string.",
CanContainSecrets = true)]
public Input<string?> ConnectionString { get; set; } = null!;
/// <summary>
/// Command to run against the database.
/// </summary>
[Input(
Description = "Command to run against the database.",
DefaultSyntax = "Sql",
UIHint = InputUIHints.CodeEditor,
UIHandler = typeof(SqlCodeOptionsProvider)
)]
public Input<string?> Command { get; set; } = null!;
/// <summary>
/// The number of affected rows.
/// </summary>
[Output(
Description = "The number of rows affected.")]
public Output<int?> Result { get; set; } = null!;
/// <summary>
/// <inheritdoc/>
/// </summary>
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var command = Command.GetOrDefault(context);
// If no command was specified, there's nothing to do.
if (string.IsNullOrWhiteSpace(command))
return;
// Get and execute the SQL evaluator.
var evaluator = context.GetRequiredService<ISqlEvaluator>();
var evaluatedQuery = await evaluator.EvaluateAsync(command, context.ExpressionExecutionContext, new ExpressionEvaluatorOptions(), context.CancellationToken);
// Create client
var factory = context.GetRequiredService<ISqlClientFactory>();
var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context));
// Execute command
var result = await client.ExecuteCommandAsync(evaluatedQuery);
context.Set(Result, result);
await CompleteAsync(context);
}
}
@@ -0,0 +1,90 @@
using System.Data;
using System.Runtime.CompilerServices;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Sql.Contracts;
using Elsa.Sql.UIHints;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Elsa.Workflows.UIHints;
namespace Elsa.Sql.Activities;
/// <summary>
/// Execute given SQL query and return the resulting data.
/// </summary>
[Activity("Elsa", "SQL", "Execute given SQL query and return the resulting data.", DisplayName = "SQL Query", Kind = ActivityKind.Task)]
public class SqlQuery : Activity
{
/// <summary>
/// <inheritdoc/>
/// </summary>
public SqlQuery([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <summary>
/// Database client to connect with.
/// </summary>
[Input(
Description = "Database client.",
UIHint = InputUIHints.DropDown,
UIHandler = typeof(SqlClientsDropDownProvider))]
public Input<string?> Client { get; set; } = null!;
/// <summary>
/// Connection string.
/// </summary>
[Input(
Description = "Connection string.",
CanContainSecrets = true)]
public Input<string?> ConnectionString { get; set; } = null!;
/// <summary>
/// Query to run against the database.
/// </summary>
[Input(
Description = "Query to run against the database.",
DefaultSyntax = "Sql",
UIHint = InputUIHints.CodeEditor,
UIHandler = typeof(SqlCodeOptionsProvider)
)]
public Input<string?> Query { get; set; } = null!;
/// <summary>
/// <see cref="DataSet"/> of queried results.
/// </summary>
[Output(
Description = "DataSet of queried results.",
IsSerializable = false)]
public Output<DataSet?> Results { get; set; } = null!;
/// <summary>
/// <inheritdoc/>
/// </summary>
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var query = Query.GetOrDefault(context);
// If no query was specified, there's nothing to do.
if (string.IsNullOrWhiteSpace(query))
return;
// Get and execute the SQL evaluator.
var evaluator = context.GetRequiredService<ISqlEvaluator>();
var evaluatedQuery = await evaluator.EvaluateAsync(query, context.ExpressionExecutionContext, new ExpressionEvaluatorOptions(), context.CancellationToken);
// Create client
var factory = context.GetRequiredService<ISqlClientFactory>();
var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context));
// Execute query
var results = await client.ExecuteQueryAsync(evaluatedQuery);
context.Set(Results, results);
await CompleteAsync(context);
}
}
@@ -0,0 +1,88 @@
using System.Runtime.CompilerServices;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Sql.Contracts;
using Elsa.Sql.UIHints;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
using Elsa.Workflows.UIHints;
namespace Elsa.Sql.Activities;
/// <summary>
/// Execute given SQL command and return a single result.
/// </summary>
[Activity("Elsa", "SQL", "Execute given SQL command and return a single result.", DisplayName = "SQL Single Value", Kind = ActivityKind.Task)]
public class SqlSingleValue : Activity
{
/// <summary>
/// <inheritdoc/>
/// </summary>
public SqlSingleValue([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <summary>
/// Database client to connect with.
/// </summary>
[Input(
Description = "Database client.",
UIHint = InputUIHints.DropDown,
UIHandler = typeof(SqlClientsDropDownProvider))]
public Input<string?> Client { get; set; } = null!;
/// <summary>
/// Connection string.
/// </summary>
[Input(
Description = "Connection string.",
CanContainSecrets = true)]
public Input<string?> ConnectionString { get; set; } = null!;
/// <summary>
/// Command to run against the database.
/// </summary>
[Input(
Description = "Query to run against the database.",
DefaultSyntax = "Sql",
UIHint = InputUIHints.CodeEditor,
UIHandler = typeof(SqlCodeOptionsProvider)
)]
public Input<string?> Command { get; set; } = null!;
/// <summary>
/// Command result.
/// </summary>
[Output(
Description = "Command result.")]
public Output<object?> Result { get; set; } = null!;
/// <summary>
/// <inheritdoc/>
/// </summary>
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var command = Command.GetOrDefault(context);
// If no command was specified, there's nothing to do.
if (string.IsNullOrWhiteSpace(command))
return;
// Get and execute the SQL evaluator.
var evaluator = context.GetRequiredService<ISqlEvaluator>();
var evaluatedQuery = await evaluator.EvaluateAsync(command, context.ExpressionExecutionContext, new ExpressionEvaluatorOptions(), context.CancellationToken);
// Create client
var factory = context.GetRequiredService<ISqlClientFactory>();
var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context));
// Execute command
var result = await client.ExecuteScalarAsync(evaluatedQuery);
context.Set(Result, result);
await CompleteAsync(context);
}
}
@@ -0,0 +1,151 @@
using System.Data;
using System.Data.Common;
using Elsa.Sql.Models;
namespace Elsa.Sql.Client;
public abstract class BaseSqlClient : ISqlClient
{
/// <summary>
/// The connection string used to connect with the database.
/// </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: "param"
/// </summary>
public virtual string ParameterText { get; set; } = "p";
/// <summary>
/// Set to true to add a counter to the end of the parameter string.
/// Default: true
/// </summary>
public virtual bool IncrementParameter { get; set; } = true;
/// <summary>
/// Default base implementation for an SQL client.
/// </summary>
/// <param name="connectionString"></param>
protected BaseSqlClient(string connectionString) => _connectionString = connectionString;
/// <summary>
/// Create a connection using the client specific connection.
/// </summary>
/// <returns></returns>
protected abstract DbConnection CreateConnection();
/// <summary>
/// Create a command using the client specific connection.
/// </summary>
/// <param name="query"></param>
/// <param name="connection"></param>
/// <returns></returns>
protected abstract DbCommand CreateCommand(string query, DbConnection connection);
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<int?> ExecuteCommandAsync(EvaluatedQuery evaluatedQuery)
{
using var connection = CreateConnection();
connection.Open();
var command = CreateCommand(evaluatedQuery.Query, connection);
AddCommandParameters(command, evaluatedQuery.Parameters);
var result = await command.ExecuteNonQueryAsync();
return result;
}
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<object?> ExecuteScalarAsync(EvaluatedQuery evaluatedQuery)
{
using var connection = CreateConnection();
connection.Open();
var command = CreateCommand(evaluatedQuery.Query, connection);
AddCommandParameters(command, evaluatedQuery.Parameters);
var result = await command.ExecuteScalarAsync();
return result;
}
/// <summary>
/// <inheritdoc/>
/// </summary>
public async Task<DataSet?> ExecuteQueryAsync(EvaluatedQuery evaluatedQuery)
{
using var connection = CreateConnection();
connection.Open();
var command = CreateCommand(evaluatedQuery.Query, connection);
AddCommandParameters(command, evaluatedQuery.Parameters);
using var reader = await command.ExecuteReaderAsync();
return await Task.FromResult(ReadAsDataSet(reader));
}
/// <summary>
/// Add 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 AddCommandParameters(DbCommand command, Dictionary<string, object?> parameters)
{
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;
}
/// <summary>
/// Returns <see cref="IDataReader"/> data as a <see cref="DataSet"/>.
/// </summary>
/// <param name="reader">Reader to return data from.</param>
/// <returns><see cref="DataSet"/> of data.</returns>
private DataSet ReadAsDataSet(IDataReader reader)
{
var dataSet = new DataSet("dataset");
dataSet.Tables.Add(ReadAsDataTable(reader));
return dataSet;
}
/// <summary>
/// Returns <see cref="IDataReader"/> data as a <see cref="DataTable"/>.
/// </summary>
/// <param name="reader">Reader to return data from.</param>
/// <returns><see cref="DataTable"/> of data.</returns>
private DataTable ReadAsDataTable(IDataReader reader)
{
var data = new DataTable();
var schemaTable =reader.GetSchemaTable();
foreach (DataRow row in schemaTable.Rows)
{
string colName = row.Field<string>("ColumnName");
Type t = row.Field<Type>("DataType");
data.Columns.Add(colName, t);
}
while (reader.Read())
{
var newRow = data.Rows.Add();
foreach (DataColumn col in data.Columns)
{
newRow[col.ColumnName] = reader[col.ColumnName];
}
}
return data;
}
}
+43
View File
@@ -0,0 +1,43 @@
using System.Data;
using Elsa.Sql.Models;
namespace Elsa.Sql.Client;
public interface ISqlClient
{
/// <summary>
/// The marker used when injecting parameters into a query.
/// </summary>
public string ParameterMarker { get; set; }
/// <summary>
/// The text following the <c>ParameterMarker</c> when injecting parameters into a query.
/// </summary>
public string ParameterText { get; set; }
/// <summary>
/// Set to true to add a counter to the end of the parameter string.
/// </summary>
public bool IncrementParameter { get; set; }
/// <summary>
/// Asynchronously executes a Transact-SQL statement against the connection and returns the number of rows affected.
/// </summary>
/// <param name="evaluatedQuery">The evaluated query to execute.</param>
/// <returns>The number of rows affected.</returns>
public Task<int?> ExecuteCommandAsync(EvaluatedQuery evaluatedQuery);
/// <summary>
/// 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.
/// </summary>
/// <param name="evaluatedQuery">The evaluated query to execute.</param>
/// <returns>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.</returns>
public Task<object?> ExecuteScalarAsync(EvaluatedQuery evaluatedQuery);
/// <summary>
/// Asynchronously executes the query, and returns a dataset of data returned by the query.
/// </summary>
/// <param name="evaluatedQuery">The evaluated query to execute.</param>
/// <returns>DataSet of the queried data</returns>
public Task<DataSet?> ExecuteQueryAsync(EvaluatedQuery evaluatedQuery);
}
@@ -0,0 +1,14 @@
using Elsa.Sql.Client;
namespace Elsa.Sql.Contracts;
public interface ISqlClientFactory
{
/// <summary>
/// Create an instance of the registered client.
/// </summary>
/// <param name="clientName">The name of the registered client to create. This can either be clientName used during registration or the default nameof(client) itself.</param>
/// <param name="connectionString">Connection string.</param>
/// <returns></returns>
public ISqlClient CreateClient(string clientName, string connectionString);
}
@@ -0,0 +1,11 @@
namespace Elsa.Sql.Contracts;
public interface ISqlClientNamesProvider
{
/// <summary>
/// Returns a dictionary of registered clients.
/// </summary>
/// <param name="cancellationToken">A token to monitor cancellation requests.</param>
/// <returns>A <see cref="Dictionary{TKey, TValue}"/> of registered client names their <see cref="Type"/>.</returns>
Task<IReadOnlyDictionary<string, Type>> GetRegisteredSqlClientNamesAsync(CancellationToken cancellationToken);
}
@@ -0,0 +1,26 @@
using Elsa.Expressions.Models;
using Elsa.Sql.Models;
using JetBrains.Annotations;
namespace Elsa.Sql.Contracts;
/// <summary>
/// Evaluates SQL expressions.
/// </summary>
[PublicAPI]
public interface ISqlEvaluator
{
/// <summary>
/// Evaluates a SQL expression.
/// </summary>
/// <param name="expression">The expression to evaluate.</param>
/// <param name="context">The context in which the expression is evaluated.</param>
/// <param name="options">A set of options.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The <see cref="EvaluatedQuery"/> result.</returns>
Task<EvaluatedQuery> EvaluateAsync(
string expression,
ExpressionExecutionContext context,
ExpressionEvaluatorOptions options,
CancellationToken cancellationToken = default);
}
+15
View File
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides activities to interact with sql databases.
</Description>
<PackageTags>elsa module activities sql</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,29 @@
using Elsa.Sql.Contracts;
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Helpers;
using Elsa.Expressions.Models;
namespace Elsa.Sql.Expressions;
/// <summary>
/// Evaluates SQL expressions.
/// </summary>
public class SqlExpressionHandler : IExpressionHandler
{
private readonly ISqlEvaluator _sqlEvaluator;
/// <summary>
/// Initializes a new instance of the <see cref="SqlExpressionHandler"/> class.
/// </summary>
public SqlExpressionHandler(ISqlEvaluator sqlEvaluator)
{
_sqlEvaluator = sqlEvaluator;
}
/// <inheritdoc />
public async ValueTask<object?> EvaluateAsync(Expression expression, Type returnType, ExpressionExecutionContext context, ExpressionEvaluatorOptions options)
{
var script = expression.Value.ConvertTo<string>() ?? "";
return await _sqlEvaluator.EvaluateAsync(script, context, options);
}
}
@@ -0,0 +1,22 @@
using Elsa.Features.Services;
using Elsa.Sql.Features;
namespace Elsa.Sql.Extensions;
/// <summary>
/// Provides methods to install and configure SQL client features.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Adds the <see cref="SqlFeature"/> feature to the system.
/// </summary>
/// <param name="configuration"></param>
/// <param name="configure"></param>
/// <returns></returns>
public static IModule UseSql(this IModule configuration, Action<SqlFeature>? configure = default)
{
configuration.Configure(configure);
return configuration;
}
}
@@ -0,0 +1,43 @@
using Elsa.Sql.Client;
using Elsa.Sql.Contracts;
using Elsa.Sql.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Sql.Factory;
/// <summary>
/// SQL client factory
/// </summary>
public class SqlClientFactory : ISqlClientFactory
{
private readonly IServiceProvider _serviceProvider;
public SqlClientFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
/// <summary>
/// <inheritdoc/>
/// </summary>
public ISqlClient CreateClient(string clientName, string connectionString)
{
if (string.IsNullOrEmpty(clientName))
{
throw new ArgumentException($"Client name can not be empty or null.", nameof(clientName));
}
if (string.IsNullOrEmpty(connectionString))
{
throw new ArgumentException($"Connection string can not be empty or null.", nameof(connectionString));
}
if (_serviceProvider.GetRequiredService<ClientStore>().Clients.TryGetValue(clientName, out var clientType))
{
try
{
return ActivatorUtilities.CreateInstance(_serviceProvider, clientType, connectionString) as ISqlClient;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Unable to create instance of '{clientName}' of type '{clientType}'.", ex);
}
}
throw new ArgumentException($"No registered SQL client provider for '{clientName}'.");
}
}
@@ -0,0 +1,63 @@
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Sql.Activities;
using Elsa.Sql.Contracts;
using Elsa.Sql.Factory;
using Elsa.Sql.Providers;
using Elsa.Sql.Services;
using Elsa.Sql.UIHints;
using Elsa.Workflows;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Sql.Features;
/// <summary>
/// Setup SQL client features
/// </summary>
public class SqlFeature : FeatureBase
{
/// <summary>
/// Set a callback to configure <see cref="ClientStore"/>.
/// </summary>
public Action<ClientStore> Clients { get; set; } = _ => { };
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <param name="module"></param>
public SqlFeature(IModule module) : base(module)
{
}
/// <summary>
/// <inheritdoc/>
/// </summary>
public override void Configure()
{
Module.AddActivitiesFrom<SqlFeature>();
}
/// <summary>
/// <inheritdoc/>
/// </summary>
public override void Apply()
{
Services
// Services
.AddSingleton(provider =>
{
ClientStore clientRegistry = new();
Clients.Invoke(clientRegistry);
return clientRegistry;
})
.AddSingleton<ISqlClientFactory, SqlClientFactory>()
.AddScoped<ISqlEvaluator, SqlEvaluator>()
// Providers
.AddExpressionDescriptorProvider<SqlExpressionDescriptorProvider>()
.AddScoped<IPropertyUIHandler, SqlCodeOptionsProvider>()
.AddScoped<IPropertyUIHandler, SqlClientsDropDownProvider>()
.AddScoped<ISqlClientNamesProvider, SqlClientNamesProvider>();
}
}
+3
View File
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,35 @@
namespace Elsa.Sql.Models
{
/// <summary>
/// Represents a safely formatted SQL expression result.
/// </summary>
public class EvaluatedQuery
{
/// <summary>
/// Query with parameterized values
/// </summary>
public string Query { get; set; }
/// <summary>
/// Parameters to inject into the query at execution
/// </summary>
public Dictionary<string, object?> Parameters { get; set; } = new Dictionary<string, object?>();
/// <summary>
/// An evaluated query response.
/// </summary>
/// <param name="query">The evaluated query</param>
public EvaluatedQuery(string query) => Query = query;
/// <summary>
/// An evaluated query response.
/// </summary>
/// <param name="query">The evaluated query</param>
/// <param name="parameters">Parameters to pass into the parameterized query</param>
public EvaluatedQuery(string query, Dictionary<string, object?> parameters)
{
Query = query;
Parameters = parameters;
}
}
}
@@ -0,0 +1,23 @@
using Elsa.Sql.Contracts;
using Elsa.Sql.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Sql.Providers;
/// <summary>
/// Returns registered client names
/// </summary>
public class SqlClientNamesProvider : ISqlClientNamesProvider
{
private readonly IServiceProvider _serviceProvider;
public SqlClientNamesProvider(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
/// <summary>
/// <inheritdoc/>
/// </summary>
public Task<IReadOnlyDictionary<string, Type>> GetRegisteredSqlClientNamesAsync(CancellationToken cancellationToken)
{
return Task.FromResult(_serviceProvider.GetRequiredService<ClientStore>().Clients);
}
}
@@ -0,0 +1,29 @@
using Elsa.Expressions.Contracts;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Sql.Expressions;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Sql.Providers;
/// <summary>
/// Provides SQL expression descriptors.
/// </summary>
public class SqlExpressionDescriptorProvider : IExpressionDescriptorProvider
{
/// <summary>
/// Gets the name of the expression type.
/// </summary>
private const string TypeName = "Sql";
public IEnumerable<ExpressionDescriptor> GetDescriptors()
{
yield return new()
{
Type = TypeName,
DisplayName = "SQL",
Properties = new { MonacoLanguage = "sql" }.ToDictionary(),
HandlerFactory = ActivatorUtilities.GetServiceOrCreateInstance<SqlExpressionHandler>
};
}
}
@@ -0,0 +1,36 @@
using Elsa.Sql.Client;
namespace Elsa.Sql.Services;
public class ClientStore
{
private readonly Dictionary<string, Type> clients = new();
/// <summary>
/// Dictionary of registered clients and their type.
/// </summary>
public IReadOnlyDictionary<string, Type> Clients => clients;
/// <summary>
/// Registers the specified client type <typeparamref name="TClient"/> with the store.
/// The client type must inherit from <see cref="ISqlClient"/>.
/// </summary>
/// <typeparam name="TClient">
/// The type of the client to be registered. The client must be a class that implements the <see cref="ISqlClient"/> interface.
/// </typeparam>
/// <param name="name">
/// The name of the client to register. If not provided, the name defaults to <c>nameof(TClient)</c>.
/// This value is used as a key to identify the client in the store.
/// </param>
/// <exception cref="InvalidOperationException">
/// Thrown when a client with the same name is already registered in the store.
/// </exception>
/// <remarks>
/// This method registers a client type to the store using a unique key. The key is either the provided <paramref name="name"/> or the default name derived from <typeparamref name="TClient"/>.
/// </remarks>
public void Register<TClient>(string? name) where TClient : class, ISqlClient
{
var key = string.IsNullOrEmpty(name) ? nameof(TClient) : name;
if (clients.ContainsKey(key)) { throw new InvalidOperationException($"Client with key '{name}' is already registered."); }
clients.Add(key, typeof(TClient));
}
}
@@ -0,0 +1,94 @@
using System.Text;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Sql.Contracts;
using Elsa.Sql.Models;
using Elsa.Workflows;
namespace Elsa.Sql.Services;
/// <summary>
/// A SQL expression evaluator.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="SqlEvaluator"/> class.WS
/// </remarks>
public class SqlEvaluator() : ISqlEvaluator
{
private WorkflowExecutionContext executionContext;
private ActivityExecutionContext activityContext;
private ExpressionExecutionContext expressionContext;
/// <inheritdoc />
public async Task<EvaluatedQuery> EvaluateAsync(
string expression,
ExpressionExecutionContext context,
ExpressionEvaluatorOptions options,
CancellationToken cancellationToken = default)
{
if (!expression.Contains("{{")) return new EvaluatedQuery(expression);
expressionContext = context;
executionContext = context.GetWorkflowExecutionContext();
activityContext = context.GetActivityExecutionContext();
// Create client
var factory = context.GetRequiredService<ISqlClientFactory>();
var client = factory.CreateClient(activityContext.ActivityState["Client"].ToString(), activityContext.ActivityState["ConnectionString"].ToString());
var sb = new StringBuilder();
int start = 0;
var parameters = new Dictionary<string, object?>();
int paramIndex = 0;
while (start < expression.Length)
{
int openIndex = expression.IndexOf("{{", start);
if (openIndex == -1)
{
sb.Append(expression.AsSpan(start));
break;
}
// Append everything before {{
sb.Append(expression.AsSpan(start, openIndex - start));
// Find the closing }}
int closeIndex = expression.IndexOf("}}", openIndex + 2);
if (closeIndex == -1) throw new FormatException("Unmatched '{{' found in SQL expression.");
// 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 and replace with parameterized name
var counterValue = client.IncrementParameter ? paramIndex++.ToString() : string.Empty;
string paramName = $"{client.ParameterMarker}{client.ParameterText}{counterValue}";
parameters[paramName] = ResolveValue(key);
sb.Append(paramName);
start = closeIndex + 2;
}
return new EvaluatedQuery(sb.ToString(), parameters);
}
private object? ResolveValue(string key)
{
return key switch
{
"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("Variable.") => expressionContext.GetVariableInScope(v.Substring(9)) ?? null,
// OBSOLETE: This is deprecated and will be removed in a future version. Use 'Variable.' instead.
var v when v.StartsWith("Variables.") => expressionContext.GetVariableInScope(v.Substring(10)) ?? null,
_ => throw new NullReferenceException($"No matching property found for {{{{{key}}}}}.")
};
}
}
@@ -0,0 +1,18 @@
using System.Reflection;
using Elsa.Sql.Contracts;
using Elsa.Workflows.UIHints.Dropdown;
namespace Elsa.Sql.UIHints;
/// <summary>
/// Provides registered clients for the Client input field.
/// </summary>
/// <param name="sqlClientNamesProvider"></param>
public class SqlClientsDropDownProvider(ISqlClientNamesProvider sqlClientNamesProvider) : DropDownOptionsProviderBase
{
protected override async ValueTask<ICollection<SelectListItem>> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken)
{
var clients = await sqlClientNamesProvider.GetRegisteredSqlClientNamesAsync(cancellationToken);
return clients.Select(x => new SelectListItem(x.Key, x.Key)).ToList();
}
}