From b0f4e2943d79132928aa10637684dcc21404b1fd Mon Sep 17 00:00:00 2001 From: Wayne Vera Date: Fri, 22 May 2026 14:02:57 +0200 Subject: [PATCH 1/5] Implement enahancement for issue #153 --- Elsa.Extensions.sln | 3 -- .../sql/Elsa.Sql/Activities/SqlQuery.cs | 25 ++++++++--- .../Contracts/ISqlClientResultTypeFactory.cs | 9 ++++ .../Contracts/ISqlClientResultTypeHandler.cs | 12 +++++ .../ISqlClientResultTypesProvider.cs | 6 +++ .../Factory/SqlClientResultTypeFactory.cs | 34 ++++++++++++++ .../sql/Elsa.Sql/Features/SqlFeature.cs | 22 ++++++++- .../Handlers/SqlClientDataSetResultHandler.cs | 23 ++++++++++ .../SqlClientRecordSetResultHandler.cs | 45 +++++++++++++++++++ .../Providers/SqlClientResultTypesProvider.cs | 23 ++++++++++ .../sql/Elsa.Sql/Services/ClientStore.cs | 18 +++++++- .../SqlClientResultTypesDropDownProvider.cs | 18 ++++++++ 12 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeFactory.cs create mode 100644 src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeHandler.cs create mode 100644 src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypesProvider.cs create mode 100644 src/modules/sql/Elsa.Sql/Factory/SqlClientResultTypeFactory.cs create mode 100644 src/modules/sql/Elsa.Sql/Handlers/SqlClientDataSetResultHandler.cs create mode 100644 src/modules/sql/Elsa.Sql/Handlers/SqlClientRecordSetResultHandler.cs create mode 100644 src/modules/sql/Elsa.Sql/Providers/SqlClientResultTypesProvider.cs create mode 100644 src/modules/sql/Elsa.Sql/UIHints/SqlClientResultTypesDropDownProvider.cs diff --git a/Elsa.Extensions.sln b/Elsa.Extensions.sln index 7a22cadf..07a132ae 100644 --- a/Elsa.Extensions.sln +++ b/Elsa.Extensions.sln @@ -613,7 +613,6 @@ Global {C4D65789-A62A-DAD5-7246-A0E476F86CDF}.Release|x86.ActiveCfg = Release|Any CPU {C4D65789-A62A-DAD5-7246-A0E476F86CDF}.Release|x86.Build.0 = Release|Any CPU {F5B2629A-E0C9-9B73-4941-DD44DD557A9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F5B2629A-E0C9-9B73-4941-DD44DD557A9B}.Debug|Any CPU.Build.0 = Debug|Any CPU {F5B2629A-E0C9-9B73-4941-DD44DD557A9B}.Debug|x64.ActiveCfg = Debug|Any CPU {F5B2629A-E0C9-9B73-4941-DD44DD557A9B}.Debug|x64.Build.0 = Debug|Any CPU {F5B2629A-E0C9-9B73-4941-DD44DD557A9B}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -769,7 +768,6 @@ Global {FE487F94-5242-C0C7-884A-3EE8FB8FC24E}.Release|x86.ActiveCfg = Release|Any CPU {FE487F94-5242-C0C7-884A-3EE8FB8FC24E}.Release|x86.Build.0 = Release|Any CPU {C2721BCB-2FB1-9227-AABB-ED768EB292FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C2721BCB-2FB1-9227-AABB-ED768EB292FD}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2721BCB-2FB1-9227-AABB-ED768EB292FD}.Debug|x64.ActiveCfg = Debug|Any CPU {C2721BCB-2FB1-9227-AABB-ED768EB292FD}.Debug|x64.Build.0 = Debug|Any CPU {C2721BCB-2FB1-9227-AABB-ED768EB292FD}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -1069,7 +1067,6 @@ Global {E6BD7D7F-3CC5-EE66-577D-19015EFD03D8}.Release|x86.ActiveCfg = Release|Any CPU {E6BD7D7F-3CC5-EE66-577D-19015EFD03D8}.Release|x86.Build.0 = Release|Any CPU {159E73C1-60F9-0D39-9CA1-79EC99187FDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {159E73C1-60F9-0D39-9CA1-79EC99187FDC}.Debug|Any CPU.Build.0 = Debug|Any CPU {159E73C1-60F9-0D39-9CA1-79EC99187FDC}.Debug|x64.ActiveCfg = Debug|Any CPU {159E73C1-60F9-0D39-9CA1-79EC99187FDC}.Debug|x64.Build.0 = Debug|Any CPU {159E73C1-60F9-0D39-9CA1-79EC99187FDC}.Debug|x86.ActiveCfg = Debug|Any CPU diff --git a/src/modules/sql/Elsa.Sql/Activities/SqlQuery.cs b/src/modules/sql/Elsa.Sql/Activities/SqlQuery.cs index 80c798de..e148df91 100644 --- a/src/modules/sql/Elsa.Sql/Activities/SqlQuery.cs +++ b/src/modules/sql/Elsa.Sql/Activities/SqlQuery.cs @@ -3,6 +3,7 @@ using Elsa.Expressions.Models; using Elsa.Extensions; using Elsa.Sql.Contracts; +using Elsa.Sql.Handlers; using Elsa.Sql.UIHints; using Elsa.Workflows; using Elsa.Workflows.Attributes; @@ -52,14 +53,23 @@ public SqlQuery([CallerFilePath] string? source = null, [CallerLineNumber] int? )] public Input Query { get; set; } = null!; + /// + /// Type of the result. The result type determines how the raw query result will be handled and returned. For example, if the result type is set to "DataSet", the raw query result will be returned as a DataSet object. If the result type is set to "RecordSet", the raw query result will be transformed into an array of records, where each record is represented as a dictionary of column names and values. + /// + [Input( + Description = "The type of the result.", + UIHint = InputUIHints.DropDown, + UIHandler = typeof(SqlClientResultTypesDropDownProvider), + DefaultValue = SqlClientDataSetResultHandler.Name)] + public Input ResultType { get; set; } = new(SqlClientDataSetResultHandler.Name); /// /// of queried results. /// [Output( - Description = "DataSet of queried results.", + Description = "Queried results.", IsSerializable = false)] - public Output Results { get; set; } = null!; + public Output Results { get; set; } = null!; /// @@ -78,12 +88,17 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context var evaluatedQuery = await evaluator.EvaluateAsync(query, context.ExpressionExecutionContext, new ExpressionEvaluatorOptions(), context.CancellationToken); // Create client - var factory = context.GetRequiredService(); - var client = factory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context)); + var clientFactory = context.GetRequiredService(); + var client = clientFactory.CreateClient(Client.GetOrDefault(context), ConnectionString.GetOrDefault(context)); // Execute query var results = await client.ExecuteQueryAsync(evaluatedQuery); - context.Set(Results, results); + + // Query Result + var resultTypeFactory = context.GetRequiredService(); + var resultTypeHandler = resultTypeFactory.CreateHandle(ResultType.GetOrDefault(context) ?? SqlClientDataSetResultHandler.Name); + var handledResult = await resultTypeHandler.HandleAsync(results, context.CancellationToken); + context.Set(Results, handledResult); await CompleteAsync(context); } diff --git a/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeFactory.cs b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeFactory.cs new file mode 100644 index 00000000..cf4d3149 --- /dev/null +++ b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeFactory.cs @@ -0,0 +1,9 @@ + +using Elsa.Sql.Client; + +namespace Elsa.Sql.Contracts; + +public interface ISqlClientResultTypeFactory +{ + ISqlClientResultTypeHandler CreateHandle(string resultType); +} \ No newline at end of file diff --git a/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeHandler.cs b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeHandler.cs new file mode 100644 index 00000000..16f88b65 --- /dev/null +++ b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeHandler.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Elsa.Sql.Contracts; + +public interface ISqlClientResultTypeHandler +{ + Task HandleAsync(object? queryResult, CancellationToken cancellationToken); +} diff --git a/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypesProvider.cs b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypesProvider.cs new file mode 100644 index 00000000..f736e6ac --- /dev/null +++ b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypesProvider.cs @@ -0,0 +1,6 @@ +namespace Elsa.Sql.Contracts; + +public interface ISqlClientResultTypesProvider +{ + Task> GetRegisteredSqlResultTypesAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/modules/sql/Elsa.Sql/Factory/SqlClientResultTypeFactory.cs b/src/modules/sql/Elsa.Sql/Factory/SqlClientResultTypeFactory.cs new file mode 100644 index 00000000..8383144c --- /dev/null +++ b/src/modules/sql/Elsa.Sql/Factory/SqlClientResultTypeFactory.cs @@ -0,0 +1,34 @@ +using Elsa.Sql.Contracts; +using Elsa.Sql.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Sql.Factory; + +/// +/// Provides a factory for creating SQL client result type handlers using dependency injection. +/// +/// The service provider used to resolve result type handlers. +public class SqlClientResultTypeFactory(IServiceProvider _serviceProvider) : ISqlClientResultTypeFactory +{ + public ISqlClientResultTypeHandler CreateHandle(string resultTypeName) + { + if (string.IsNullOrEmpty(resultTypeName)) + { + throw new ArgumentException($"Result type can not be empty or null.", nameof(resultTypeName)); + } + if (_serviceProvider.GetRequiredService().ResultTypes.TryGetValue(resultTypeName, out var resultHandlerType)) + { + try + { + return ActivatorUtilities.CreateInstance(_serviceProvider, resultHandlerType) as ISqlClientResultTypeHandler ?? throw new KeyNotFoundException($"Result handler type '{resultTypeName}' not found."); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Unable to create instance of '{resultTypeName}' of type '{resultHandlerType}'.", ex); + } + } + throw new ArgumentException($"No registered SQL result type for '{resultTypeName}'."); + } + + +} diff --git a/src/modules/sql/Elsa.Sql/Features/SqlFeature.cs b/src/modules/sql/Elsa.Sql/Features/SqlFeature.cs index 882f1c50..d9f01811 100644 --- a/src/modules/sql/Elsa.Sql/Features/SqlFeature.cs +++ b/src/modules/sql/Elsa.Sql/Features/SqlFeature.cs @@ -4,6 +4,7 @@ using Elsa.Sql.Activities; using Elsa.Sql.Contracts; using Elsa.Sql.Factory; +using Elsa.Sql.Handlers; using Elsa.Sql.Providers; using Elsa.Sql.Services; using Elsa.Sql.UIHints; @@ -22,6 +23,8 @@ public class SqlFeature : FeatureBase /// public Action Clients { get; set; } = _ => { }; + public Action ResultTypes { get; set; } = _ => { }; + /// /// /// @@ -48,16 +51,33 @@ public override void Apply() .AddSingleton(provider => { ClientStore clientRegistry = new(); + Clients.Invoke(clientRegistry); + ResultTypes.Invoke(clientRegistry); + + if (clientRegistry.ResultTypes.Count == 0) + clientRegistry.RegisterResultHandler(SqlClientRecordSetResultHandler.Name); + + if (!clientRegistry.ResultTypes.ContainsKey(SqlClientDataSetResultHandler.Name)) + clientRegistry.RegisterResultHandler(SqlClientDataSetResultHandler.Name); + return clientRegistry; }) .AddSingleton() + .AddSingleton() .AddScoped() // Providers .AddExpressionDescriptorProvider() .AddScoped() .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped(); + + + + } } \ No newline at end of file diff --git a/src/modules/sql/Elsa.Sql/Handlers/SqlClientDataSetResultHandler.cs b/src/modules/sql/Elsa.Sql/Handlers/SqlClientDataSetResultHandler.cs new file mode 100644 index 00000000..20317380 --- /dev/null +++ b/src/modules/sql/Elsa.Sql/Handlers/SqlClientDataSetResultHandler.cs @@ -0,0 +1,23 @@ +using System.Data; +using Elsa.Sql.Contracts; + +namespace Elsa.Sql.Handlers; + +/// +/// Handler for query results of type . This handler simply returns the raw object as the result of the query. It does not perform any transformation or processing on the data, allowing consumers to work with the full capabilities of the class when handling the query results. +/// +public class SqlClientDataSetResultHandler : ISqlClientResultTypeHandler +{ + public const string Name = "DataSet"; + + public Task HandleAsync(object? queryResult, CancellationToken cancellationToken) + { + if (queryResult is null) + return Task.FromResult(null); + + if (queryResult is not DataSet dataSet) + throw new InvalidOperationException($"Expected query result to be of type {typeof(DataSet).FullName}, but got {queryResult.GetType().FullName}."); + + return Task.FromResult(dataSet); + } +} diff --git a/src/modules/sql/Elsa.Sql/Handlers/SqlClientRecordSetResultHandler.cs b/src/modules/sql/Elsa.Sql/Handlers/SqlClientRecordSetResultHandler.cs new file mode 100644 index 00000000..95260def --- /dev/null +++ b/src/modules/sql/Elsa.Sql/Handlers/SqlClientRecordSetResultHandler.cs @@ -0,0 +1,45 @@ +using System.Data; +using Elsa.Extensions; +using Elsa.Sql.Contracts; + +namespace Elsa.Sql.Handlers; + +/// +/// Handler for query results of type "RecordSet". This handler transforms the raw query result, which is expected to be a containing a single , into an array of records. Each record is represented as a dictionary where the keys are the column names and the values are the corresponding values for each column in that row. This allows consumers to work with the query results in a more flexible and dynamic way, without being tied to the structure of a DataSet or DataTable. +/// +public class SqlClientRecordSetResultHandler : ISqlClientResultTypeHandler +{ + public const string Name = "RecordSet"; + + public Task HandleAsync(object? queryResult, CancellationToken cancellationToken) + { + if (queryResult is null) + return Task.FromResult(null); + + if (queryResult is not DataSet dataSet) + throw new InvalidOperationException($"Expected query result to be of type {typeof(DataSet).FullName}, but got {queryResult.GetType().FullName}."); + + if (dataSet.Tables.Count == 0) + return Task.FromResult(null); + + if (dataSet.Tables.Count > 1) + throw new InvalidOperationException($"Expected query result to contain only one DataTable, but got {dataSet.Tables.Count}."); + + DataTable dataTable = dataSet.Tables[0]; + + List> result = new List>(dataTable.Rows.Count); + IEnumerable columns = dataTable.Columns.Cast(); + foreach (DataRow dr in dataTable.Rows) + { + Dictionary row = new (dataTable.Columns.Count); + row.AddRange(dataTable.Columns.Cast().Select(col => + { + object value = dr[col]; + return new KeyValuePair(col.ColumnName, value == DBNull.Value ? null : value); + })); + result.Add(row); + } + + return Task.FromResult(result.ToArray()); + } +} diff --git a/src/modules/sql/Elsa.Sql/Providers/SqlClientResultTypesProvider.cs b/src/modules/sql/Elsa.Sql/Providers/SqlClientResultTypesProvider.cs new file mode 100644 index 00000000..cb22077b --- /dev/null +++ b/src/modules/sql/Elsa.Sql/Providers/SqlClientResultTypesProvider.cs @@ -0,0 +1,23 @@ +using Elsa.Sql.Contracts; +using Elsa.Sql.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Elsa.Sql.Providers; + +/// +/// Returns registered result types for SQL clients +/// +public class SqlClientResultTypesProvider : ISqlClientResultTypesProvider +{ + private readonly IServiceProvider _serviceProvider; + + public SqlClientResultTypesProvider(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; + + /// + /// + /// + public Task> GetRegisteredSqlResultTypesAsync(CancellationToken cancellationToken) + { + return Task.FromResult(_serviceProvider.GetRequiredService().ResultTypes); + } +} \ No newline at end of file diff --git a/src/modules/sql/Elsa.Sql/Services/ClientStore.cs b/src/modules/sql/Elsa.Sql/Services/ClientStore.cs index d76727ae..0779f4cd 100644 --- a/src/modules/sql/Elsa.Sql/Services/ClientStore.cs +++ b/src/modules/sql/Elsa.Sql/Services/ClientStore.cs @@ -1,15 +1,24 @@ -using Elsa.Sql.Client; +using System.Net.Sockets; +using Elsa.Sql.Client; +using Elsa.Sql.Contracts; namespace Elsa.Sql.Services; public class ClientStore { private readonly Dictionary clients = new(); + private readonly Dictionary resultTypes = new(); + /// /// Dictionary of registered clients and their type. /// public IReadOnlyDictionary Clients => clients; + /// + /// Dictionary of registered result type handlers and their type. + /// + public IReadOnlyDictionary ResultTypes => resultTypes; + /// /// Registers the specified client type with the store. /// The client type must inherit from . @@ -33,4 +42,11 @@ public void Register(string? name) where TClient : class, ISqlClient if (clients.ContainsKey(key)) { throw new InvalidOperationException($"Client with key '{name}' is already registered."); } clients.Add(key, typeof(TClient)); } + + public void RegisterResultHandler(string? name) where TResultHandler : class, ISqlClientResultTypeHandler + { + var key = string.IsNullOrEmpty(name) ? nameof(TResultHandler) : name; + if (resultTypes.ContainsKey(key)) { throw new InvalidOperationException($"Result type handler with key '{key}' is already registered."); } + resultTypes.Add(key, typeof(TResultHandler)); + } } \ No newline at end of file diff --git a/src/modules/sql/Elsa.Sql/UIHints/SqlClientResultTypesDropDownProvider.cs b/src/modules/sql/Elsa.Sql/UIHints/SqlClientResultTypesDropDownProvider.cs new file mode 100644 index 00000000..37ab2027 --- /dev/null +++ b/src/modules/sql/Elsa.Sql/UIHints/SqlClientResultTypesDropDownProvider.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using Elsa.Sql.Contracts; +using Elsa.Workflows.UIHints.Dropdown; + +namespace Elsa.Sql.UIHints; + +/// +/// Provides registered result types for the ResultType input field. +/// +/// +public class SqlClientResultTypesDropDownProvider(ISqlClientResultTypesProvider sqlClientResultTypesProvider) : DropDownOptionsProviderBase +{ + protected override async ValueTask> GetItemsAsync(PropertyInfo propertyInfo, object? context, CancellationToken cancellationToken) + { + var clients = await sqlClientResultTypesProvider.GetRegisteredSqlResultTypesAsync(cancellationToken); + return clients.Select(x => new SelectListItem(x.Key, x.Key)).ToList(); + } +} \ No newline at end of file From 5443346b263775111fd507eefcb15e4aa09b519b Mon Sep 17 00:00:00 2001 From: veraw Date: Fri, 22 May 2026 14:09:36 +0200 Subject: [PATCH 2/5] Add xml comments for contracts --- .../sql/Elsa.Sql/Contracts/ISqlClientResultTypeFactory.cs | 5 +++++ .../sql/Elsa.Sql/Contracts/ISqlClientResultTypeHandler.cs | 6 ++++++ .../sql/Elsa.Sql/Contracts/ISqlClientResultTypesProvider.cs | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeFactory.cs b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeFactory.cs index cf4d3149..1b82ff05 100644 --- a/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeFactory.cs +++ b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeFactory.cs @@ -5,5 +5,10 @@ namespace Elsa.Sql.Contracts; public interface ISqlClientResultTypeFactory { + /// + /// Creates a result type handler for the specified SQL result type. + /// + /// The name of the SQL result type. + /// An instance of a result type handler for the specified type. ISqlClientResultTypeHandler CreateHandle(string resultType); } \ No newline at end of file diff --git a/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeHandler.cs b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeHandler.cs index 16f88b65..717d864d 100644 --- a/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeHandler.cs +++ b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypeHandler.cs @@ -8,5 +8,11 @@ namespace Elsa.Sql.Contracts; public interface ISqlClientResultTypeHandler { + /// + /// Handles the result of a SQL query execution and transforms it into the desired format or type. + /// + /// The result of the SQL query execution. + /// A token to monitor for cancellation requests. + /// The transformed result. Task HandleAsync(object? queryResult, CancellationToken cancellationToken); } diff --git a/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypesProvider.cs b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypesProvider.cs index f736e6ac..9da76443 100644 --- a/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypesProvider.cs +++ b/src/modules/sql/Elsa.Sql/Contracts/ISqlClientResultTypesProvider.cs @@ -2,5 +2,10 @@ public interface ISqlClientResultTypesProvider { + /// + /// Gets a dictionary of registered SQL result types and their corresponding .NET types. + /// + /// A token to monitor for cancellation requests. + /// A dictionary mapping SQL result type names to their corresponding .NET types. Task> GetRegisteredSqlResultTypesAsync(CancellationToken cancellationToken); } \ No newline at end of file From 7932066de8831d76b2cfe12b82b01058516cd0e3 Mon Sep 17 00:00:00 2001 From: veraw Date: Fri, 22 May 2026 14:13:44 +0200 Subject: [PATCH 3/5] Align the ResultsType naming and description --- src/modules/sql/Elsa.Sql/Activities/SqlQuery.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/sql/Elsa.Sql/Activities/SqlQuery.cs b/src/modules/sql/Elsa.Sql/Activities/SqlQuery.cs index e148df91..0ad1a9ec 100644 --- a/src/modules/sql/Elsa.Sql/Activities/SqlQuery.cs +++ b/src/modules/sql/Elsa.Sql/Activities/SqlQuery.cs @@ -54,14 +54,14 @@ public SqlQuery([CallerFilePath] string? source = null, [CallerLineNumber] int? public Input Query { get; set; } = null!; /// - /// Type of the result. The result type determines how the raw query result will be handled and returned. For example, if the result type is set to "DataSet", the raw query result will be returned as a DataSet object. If the result type is set to "RecordSet", the raw query result will be transformed into an array of records, where each record is represented as a dictionary of column names and values. + /// Type of the results. The results type determines how the raw query result will be handled and returned. For example, if the results type is set to "DataSet", the raw query result will be returned as a DataSet object. If the results type is set to "RecordSet", the raw query result will be transformed into an array of records, where each record is represented as a dictionary of column names and values. /// [Input( - Description = "The type of the result.", + Description = "The type (format) to return the queried results in.", UIHint = InputUIHints.DropDown, UIHandler = typeof(SqlClientResultTypesDropDownProvider), DefaultValue = SqlClientDataSetResultHandler.Name)] - public Input ResultType { get; set; } = new(SqlClientDataSetResultHandler.Name); + public Input ResultsType { get; set; } = new(SqlClientDataSetResultHandler.Name); /// /// of queried results. @@ -96,7 +96,7 @@ protected override async ValueTask ExecuteAsync(ActivityExecutionContext context // Query Result var resultTypeFactory = context.GetRequiredService(); - var resultTypeHandler = resultTypeFactory.CreateHandle(ResultType.GetOrDefault(context) ?? SqlClientDataSetResultHandler.Name); + var resultTypeHandler = resultTypeFactory.CreateHandle(ResultsType.GetOrDefault(context) ?? SqlClientDataSetResultHandler.Name); var handledResult = await resultTypeHandler.HandleAsync(results, context.CancellationToken); context.Set(Results, handledResult); From 12ad0831a4f84bdcc1103c74e9852b4f4328287b Mon Sep 17 00:00:00 2001 From: veraw Date: Fri, 22 May 2026 16:06:51 +0200 Subject: [PATCH 4/5] Ignore extra sqlite file types linked to .db --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4421e310..d0defe89 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ *.sln.docstates .idea .DS_Store -elsa.sqlite.db +elsa.sqlite.db* # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs From 29e350e94256b97ee453dcb19effbe5fe170b15d Mon Sep 17 00:00:00 2001 From: veraw Date: Fri, 22 May 2026 22:39:53 +0200 Subject: [PATCH 5/5] Add Elsa.Sql.Tests (validate SqlClientDataSetResultHandler and SqlClientRecordSetResultHandler) --- Elsa.Extensions.sln | 91 +++++++------------ .../sql/Elsa.Sql.Tests/Elsa.Sql.Tests.csproj | 5 + .../sql/Elsa.Sql.Tests/FodyWeavers.xml | 3 + .../sql/Elsa.Sql.Tests/GlobalUsings.cs | 0 .../SqlClientDataSetResultHandlerTests.cs | 37 ++++++++ .../SqlClientRecordSetResultHandlerTests.cs | 63 +++++++++++++ 6 files changed, 142 insertions(+), 57 deletions(-) create mode 100644 test/modules/sql/Elsa.Sql.Tests/Elsa.Sql.Tests.csproj create mode 100644 test/modules/sql/Elsa.Sql.Tests/FodyWeavers.xml create mode 100644 test/modules/sql/Elsa.Sql.Tests/GlobalUsings.cs create mode 100644 test/modules/sql/Elsa.Sql.Tests/Handlers/SqlClientDataSetResultHandlerTests.cs create mode 100644 test/modules/sql/Elsa.Sql.Tests/Handlers/SqlClientRecordSetResultHandlerTests.cs diff --git a/Elsa.Extensions.sln b/Elsa.Extensions.sln index 07a132ae..a5cf0663 100644 --- a/Elsa.Extensions.sln +++ b/Elsa.Extensions.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.12.35527.113 +# Visual Studio Version 18 +VisualStudioVersion = 18.6.11819.183 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{527248D6-B851-4C8D-8667-E2FB0A91DABF}" EndProject @@ -10,13 +9,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{DE .gitignore = .gitignore CONTRIBUTING.md = CONTRIBUTING.md Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets Directory.Packages.props = Directory.Packages.props icon.png = icon.png LICENSE = LICENSE NuGet.Config = NuGet.Config README-TEMPLATE.md = README-TEMPLATE.md README.md = README.md - Directory.Build.targets = Directory.Build.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A99FA26E-2098-403A-BD04-6BBCFBE3AC7D}" @@ -25,9 +24,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{D02123 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{3948BAF0-023F-4B43-8E77-56C3B00C6EFD}" ProjectSection(SolutionItems) = preProject + .github\workflows\copilot-setup-steps.yml.yml = .github\workflows\copilot-setup-steps.yml.yml .github\workflows\packages.yml = .github\workflows\packages.yml .github\workflows\pr.yml = .github\workflows\pr.yml - .github\workflows\copilot-setup-steps.yml.yml = .github\workflows\copilot-setup-steps.yml.yml EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "_build", "build\_build.csproj", "{4D16DD17-0BC9-476D-9B38-0A8644DD92FE}" @@ -302,6 +301,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "agents", "agents", "{0125BA .github\agents\release-notes.agent.md = .github\agents\release-notes.agent.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sql", "sql", "{9500776D-B47D-4A98-8090-B1CDEDB54367}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Sql.Tests", "test\modules\sql\Elsa.Sql.Tests\Elsa.Sql.Tests.csproj", "{EC306CED-F086-46B6-6B85-7697AE1F9DA1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1373,62 +1376,22 @@ Global {7055576E-1144-4726-BA88-DF89F2A13FA8}.Debug|x86.Build.0 = Debug|Any CPU {7055576E-1144-4726-BA88-DF89F2A13FA8}.Release|Any CPU.ActiveCfg = Release|Any CPU {7055576E-1144-4726-BA88-DF89F2A13FA8}.Release|Any CPU.Build.0 = Release|Any CPU - {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Release|Any CPU.Build.0 = Release|Any CPU {7055576E-1144-4726-BA88-DF89F2A13FA8}.Release|x64.ActiveCfg = Release|Any CPU {7055576E-1144-4726-BA88-DF89F2A13FA8}.Release|x64.Build.0 = Release|Any CPU {7055576E-1144-4726-BA88-DF89F2A13FA8}.Release|x86.ActiveCfg = Release|Any CPU {7055576E-1144-4726-BA88-DF89F2A13FA8}.Release|x86.Build.0 = Release|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Debug|x64.ActiveCfg = Debug|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Debug|x64.Build.0 = Debug|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Debug|x86.ActiveCfg = Debug|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Debug|x86.Build.0 = Debug|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Release|Any CPU.Build.0 = Release|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Release|x64.ActiveCfg = Release|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Release|x64.Build.0 = Release|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Release|x86.ActiveCfg = Release|Any CPU - {A904A0FC-9D73-4349-90A3-498053B43CD6}.Release|x86.Build.0 = Release|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Debug|x64.ActiveCfg = Debug|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Debug|x64.Build.0 = Debug|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Debug|x86.ActiveCfg = Debug|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Debug|x86.Build.0 = Debug|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Release|Any CPU.Build.0 = Release|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Release|x64.ActiveCfg = Release|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Release|x64.Build.0 = Release|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Release|x86.ActiveCfg = Release|Any CPU - {E87F26AD-1F6B-4723-A3B8-4EC761B08382}.Release|x86.Build.0 = Release|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Debug|x64.ActiveCfg = Debug|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Debug|x64.Build.0 = Debug|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Debug|x86.ActiveCfg = Debug|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Debug|x86.Build.0 = Debug|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Release|Any CPU.Build.0 = Release|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Release|x64.ActiveCfg = Release|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Release|x64.Build.0 = Release|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Release|x86.ActiveCfg = Release|Any CPU - {C3D4E5F6-7890-1234-5678-90ABCDEF1234}.Release|x86.Build.0 = Release|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Debug|x64.ActiveCfg = Debug|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Debug|x64.Build.0 = Debug|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Debug|x86.ActiveCfg = Debug|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Debug|x86.Build.0 = Debug|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Release|Any CPU.Build.0 = Release|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Release|x64.ActiveCfg = Release|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Release|x64.Build.0 = Release|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Release|x86.ActiveCfg = Release|Any CPU - {AF9D6B51-19A5-451F-8225-2F1348835A3A}.Release|x86.Build.0 = Release|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Debug|x64.ActiveCfg = Debug|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Debug|x64.Build.0 = Debug|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Debug|x86.ActiveCfg = Debug|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Debug|x86.Build.0 = Debug|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Release|Any CPU.Build.0 = Release|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Release|x64.ActiveCfg = Release|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Release|x64.Build.0 = Release|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Release|x86.ActiveCfg = Release|Any CPU + {E9388ADF-B222-44CC-83AF-64BF6F07C808}.Release|x86.Build.0 = Release|Any CPU {65B9F528-568B-4E0E-894E-F22E0B7C64BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {65B9F528-568B-4E0E-894E-F22E0B7C64BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {65B9F528-568B-4E0E-894E-F22E0B7C64BB}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -1453,6 +1416,18 @@ Global {50798373-8A89-4159-BFD9-830D259538D6}.Release|x64.Build.0 = Release|Any CPU {50798373-8A89-4159-BFD9-830D259538D6}.Release|x86.ActiveCfg = Release|Any CPU {50798373-8A89-4159-BFD9-830D259538D6}.Release|x86.Build.0 = Release|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Debug|x64.Build.0 = Debug|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Debug|x86.Build.0 = Debug|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Release|Any CPU.Build.0 = Release|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Release|x64.ActiveCfg = Release|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Release|x64.Build.0 = Release|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Release|x86.ActiveCfg = Release|Any CPU + {EC306CED-F086-46B6-6B85-7697AE1F9DA1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1584,6 +1559,8 @@ Global {65B9F528-568B-4E0E-894E-F22E0B7C64BB} = {6E46B8AD-FA47-474F-B959-0B530771C28C} {50798373-8A89-4159-BFD9-830D259538D6} = {6E46B8AD-FA47-474F-B959-0B530771C28C} {0125BAC5-45B7-4E9B-8A2B-55F075BF537A} = {D0212324-351E-4CA6-95EE-27754B5367CC} + {9500776D-B47D-4A98-8090-B1CDEDB54367} = {3DDE6F89-531C-47F8-9CD7-7A4E6984FA48} + {EC306CED-F086-46B6-6B85-7697AE1F9DA1} = {9500776D-B47D-4A98-8090-B1CDEDB54367} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {11A771DA-B728-445E-8A88-AE1C84C3B3A6} diff --git a/test/modules/sql/Elsa.Sql.Tests/Elsa.Sql.Tests.csproj b/test/modules/sql/Elsa.Sql.Tests/Elsa.Sql.Tests.csproj new file mode 100644 index 00000000..39b991b8 --- /dev/null +++ b/test/modules/sql/Elsa.Sql.Tests/Elsa.Sql.Tests.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/test/modules/sql/Elsa.Sql.Tests/FodyWeavers.xml b/test/modules/sql/Elsa.Sql.Tests/FodyWeavers.xml new file mode 100644 index 00000000..00e1d9a1 --- /dev/null +++ b/test/modules/sql/Elsa.Sql.Tests/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/test/modules/sql/Elsa.Sql.Tests/GlobalUsings.cs b/test/modules/sql/Elsa.Sql.Tests/GlobalUsings.cs new file mode 100644 index 00000000..e69de29b diff --git a/test/modules/sql/Elsa.Sql.Tests/Handlers/SqlClientDataSetResultHandlerTests.cs b/test/modules/sql/Elsa.Sql.Tests/Handlers/SqlClientDataSetResultHandlerTests.cs new file mode 100644 index 00000000..9311c861 --- /dev/null +++ b/test/modules/sql/Elsa.Sql.Tests/Handlers/SqlClientDataSetResultHandlerTests.cs @@ -0,0 +1,37 @@ +using Elsa.Sql.Handlers; +using Microsoft.Extensions.Logging; + +namespace Elsa.Sql.Tests.Handlers; + +public class SqlClientDataSetResultHandlerTests +{ + [Fact] + public async Task Handle_DataSetPassedThrough() + { + var dataSet = new System.Data.DataSet(); + var table = dataSet.Tables.Add("TestTable"); + table.Columns.Add("Id", typeof(int)); + table.Columns.Add("Name", typeof(string)); + table.Rows.Add(new object[] { 1, "Test" }); + table.Rows.Add(new object[] { 2, "Another Test" }); + + var handler = new SqlClientDataSetResultHandler(); + var result = await handler.HandleAsync(dataSet, CancellationToken.None); + Assert.Equal(dataSet, result); + } + + [Fact] + public async Task Handle_NullPassedThrough() + { + var handler = new SqlClientDataSetResultHandler(); + var result = await handler.HandleAsync(null, CancellationToken.None); + Assert.Null(result); + } + + [Fact] + public async Task Handle_NonDataSetException() + { + var handler = new SqlClientDataSetResultHandler(); + await Assert.ThrowsAsync(async () => await handler.HandleAsync(new object(), CancellationToken.None)); + } +} \ No newline at end of file diff --git a/test/modules/sql/Elsa.Sql.Tests/Handlers/SqlClientRecordSetResultHandlerTests.cs b/test/modules/sql/Elsa.Sql.Tests/Handlers/SqlClientRecordSetResultHandlerTests.cs new file mode 100644 index 00000000..f30dbedb --- /dev/null +++ b/test/modules/sql/Elsa.Sql.Tests/Handlers/SqlClientRecordSetResultHandlerTests.cs @@ -0,0 +1,63 @@ +using Elsa.Sql.Handlers; +using Microsoft.Extensions.Logging; + +namespace Elsa.Sql.Tests.Handlers; + +public class SqlClientRecordSetResultHandlerTests +{ + [Fact] + public async Task Handle_DataSetAsArrayOfDictionaryObjects() + { + var dataSet = new System.Data.DataSet(); + var table = dataSet.Tables.Add("TestTable"); + table.Columns.Add("Id", typeof(int)); + table.Columns.Add("Name", typeof(string)); + table.Rows.Add(new object[] { 1, "Test" }); + table.Rows.Add(new object[] { 2, "Another Test" }); + + var handler = new SqlClientRecordSetResultHandler(); + var result = await handler.HandleAsync(dataSet, CancellationToken.None); + + Assert.NotNull(result); + Assert.IsType[]>(result); + Assert.Equal(2, ((Dictionary[])result).Length); + Assert.Equal(1, ((Dictionary[])result)[0]["Id"]); + Assert.Equal("Test", ((Dictionary[])result)[0]["Name"]); + Assert.Equal(2, ((Dictionary[])result)[1]["Id"]); + Assert.Equal("Another Test", ((Dictionary[])result)[1]["Name"]); + } + + [Fact] + public async Task Handle_NullPassedThrough() + { + var handler = new SqlClientRecordSetResultHandler(); + var result = await handler.HandleAsync(null, CancellationToken.None); + Assert.Null(result); + } + + [Fact] + public async Task Handle_NonDataSetException() + { + var handler = new SqlClientRecordSetResultHandler(); + await Assert.ThrowsAsync(async () => await handler.HandleAsync(new object(), CancellationToken.None)); + } + + [Fact] + public async Task Handle_NoTables() + { + var dataSet = new System.Data.DataSet(); + var handler = new SqlClientRecordSetResultHandler(); + var result = await handler.HandleAsync(dataSet, CancellationToken.None); + Assert.Null(result); + } + + [Fact] + public async Task Handle_MoreThanOneTable() + { + var dataSet = new System.Data.DataSet(); + dataSet.Tables.Add("Table1"); + dataSet.Tables.Add("Table2"); + var handler = new SqlClientRecordSetResultHandler(); + await Assert.ThrowsAsync(async () => await handler.HandleAsync(dataSet, CancellationToken.None)); + } +} \ No newline at end of file