From 330dc1a0d7b40cbb3cde7097ce9b4c4bb8ae8811 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 25 Feb 2024 20:36:31 +0100 Subject: [PATCH] Fix Dapper Provider (#4998) * Update Dapper for better support and pagination The update includes a significant enhancement to the Dapper module. Improvements were made to support SQL Server and enhanced SQL query pagination. Additionally, table column size was increased for various fields to handle more extensive data. Minor refactoring was also done to improve code readability. * Toggle SQL Server usage in Elsa Server The previous setting had SQL Server usage in Elsa server enabled by default. This commit changes the useSqlServer constant in Elsa.Server.Web to false, effectively switching SQL Server off unless manually activated. --- src/bundles/Elsa.Server.Web/Program.cs | 39 ++++++++---- .../Identity/Initial.cs | 7 ++- .../Management/Initial.cs | 23 +++---- .../Elsa.Dapper.Migrations/Runtime/Initial.cs | 62 ++++++++++--------- .../Elsa.Dapper.Migrations/Runtime/V3_1.cs | 5 +- .../Abstractions/SqlDialectBase.cs | 32 +++++++++- .../Elsa.Dapper/Contracts/ISqlDialect.cs | 8 +++ .../Elsa.Dapper/Dialects/SqlServerDialect.cs | 4 +- .../Elsa.Dapper/Dialects/SqliteDialect.cs | 31 ++++++++++ .../ParameterizedQueryBuilderExtensions.cs | 34 +++++----- .../Elsa.Dapper/Features/DapperFeature.cs | 2 - .../Features/DapperMigrationsFeature.cs | 19 +++++- .../Services/SqliteDbConnectionProvider.cs | 15 ++++- src/modules/Elsa.Dapper/Services/Store.cs | 2 +- 14 files changed, 201 insertions(+), 82 deletions(-) diff --git a/src/bundles/Elsa.Server.Web/Program.cs b/src/bundles/Elsa.Server.Web/Program.cs index ec8a8263b..cf1b19952 100644 --- a/src/bundles/Elsa.Server.Web/Program.cs +++ b/src/bundles/Elsa.Server.Web/Program.cs @@ -54,7 +54,13 @@ services if (useDapper) elsa.UseDapper(dapper => { - dapper.UseMigrations(); + dapper.UseMigrations(feature => + { + if (useSqlServer) + feature.UseSqlServer(); + else + feature.UseSqlite(); + }); dapper.DbConnectionProvider = sp => { if (useSqlServer) @@ -108,8 +114,8 @@ services else ef.UseSqlite(sqliteConnectionString); }); - - if(useZipCompression) + + if (useZipCompression) management.SetCompressionAlgorithm(nameof(Zstd)); }) .UseWorkflowRuntime(runtime => @@ -138,10 +144,11 @@ services }); } - if(useMassTransit) + if (useMassTransit) { runtime.UseMassTransitDispatcher(); } + runtime.WorkflowInboxCleanupOptions = options => configuration.GetSection("Runtime:WorkflowInboxCleanup").Bind(options); runtime.WorkflowDispatcherOptions = options => configuration.GetSection("Runtime:WorkflowDispatcher").Bind(options); }) @@ -187,19 +194,29 @@ services .UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options)) .UseAlterations(alterations => { - alterations.UseEntityFrameworkCore(ef => + if (useMongoDb) { - if (useSqlServer) - ef.UseSqlServer(sqlServerConnectionString); - else - ef.UseSqlite(sqliteConnectionString); - }); + // TODO: alterations.UseMongoDb(); + } + else if (useDapper) + { + // TODO: alterations.UseDapper(); + } + else + { + alterations.UseEntityFrameworkCore(ef => + { + if (useSqlServer) + ef.UseSqlServer(sqlServerConnectionString); + else + ef.UseSqlite(sqliteConnectionString); + }); + } if (useMassTransit) { alterations.UseMassTransitDispatcher(); } - }) .UseWorkflowContexts(); diff --git a/src/modules/Elsa.Dapper.Migrations/Identity/Initial.cs b/src/modules/Elsa.Dapper.Migrations/Identity/Initial.cs index cabb59edf..a01fd1cc4 100644 --- a/src/modules/Elsa.Dapper.Migrations/Identity/Initial.cs +++ b/src/modules/Elsa.Dapper.Migrations/Identity/Initial.cs @@ -1,5 +1,6 @@ using FluentMigrator; using JetBrains.Annotations; +using static System.Int32; namespace Elsa.Dapper.Migrations.Identity; @@ -17,13 +18,13 @@ public class Initial : Migration .WithColumn("Name").AsString().NotNullable() .WithColumn("HashedPassword").AsString().NotNullable() .WithColumn("HashedPasswordSalt").AsString().NotNullable() - .WithColumn("Roles").AsString().NotNullable(); + .WithColumn("Roles").AsString(MaxValue).NotNullable(); Create .Table("Roles") .WithColumn("Id").AsString().PrimaryKey() .WithColumn("Name").AsString().NotNullable() - .WithColumn("Permissions").AsString().NotNullable(); + .WithColumn("Permissions").AsString(MaxValue).NotNullable(); Create .Table("Applications") @@ -34,7 +35,7 @@ public class Initial : Migration .WithColumn("Name").AsString().NotNullable() .WithColumn("HashedApiKey").AsString().NotNullable() .WithColumn("HashedApiKeySalt").AsString().NotNullable() - .WithColumn("Roles").AsString().NotNullable(); + .WithColumn("Roles").AsString(MaxValue).NotNullable(); } /// diff --git a/src/modules/Elsa.Dapper.Migrations/Management/Initial.cs b/src/modules/Elsa.Dapper.Migrations/Management/Initial.cs index 1e7a94e64..b7aa69db9 100644 --- a/src/modules/Elsa.Dapper.Migrations/Management/Initial.cs +++ b/src/modules/Elsa.Dapper.Migrations/Management/Initial.cs @@ -1,5 +1,6 @@ using FluentMigrator; using JetBrains.Annotations; +using static System.Int32; namespace Elsa.Dapper.Migrations.Management; @@ -18,14 +19,14 @@ public class Initial : Migration .WithColumn("DefinitionId").AsString().NotNullable() .WithColumn("Name").AsString().Nullable() .WithColumn("ToolVersion").AsString().Nullable() - .WithColumn("Description").AsString().Nullable() + .WithColumn("Description").AsString(MaxValue).Nullable() .WithColumn("ProviderName").AsString().Nullable() .WithColumn("MaterializerName").AsString().NotNullable() - .WithColumn("MaterializerContext").AsString().Nullable() - .WithColumn("Props").AsString().NotNullable() + .WithColumn("MaterializerContext").AsString(MaxValue).Nullable() + .WithColumn("Props").AsString(MaxValue).NotNullable() .WithColumn("UsableAsActivity").AsBoolean().Nullable() - .WithColumn("StringData").AsString().Nullable() - .WithColumn("BinaryData").AsBinary().Nullable() + .WithColumn("StringData").AsString(MaxValue).Nullable() + .WithColumn("BinaryData").AsBinary(MaxValue).Nullable() .WithColumn("CreatedAt").AsDateTimeOffset().NotNullable() .WithColumn("Version").AsInt32().NotNullable() .WithColumn("IsLatest").AsBoolean().NotNullable() @@ -40,14 +41,14 @@ public class Initial : Migration .WithColumn("DefinitionId").AsString().NotNullable() .WithColumn("Name").AsString().Nullable() .WithColumn("ToolVersion").AsString().Nullable() - .WithColumn("Description").AsString().Nullable() + .WithColumn("Description").AsString(MaxValue).Nullable() .WithColumn("ProviderName").AsString().Nullable() .WithColumn("MaterializerName").AsString().NotNullable() .WithColumn("MaterializerContext").AsString().Nullable() - .WithColumn("Props").AsString().NotNullable() + .WithColumn("Props").AsString(MaxValue).NotNullable() .WithColumn("UsableAsActivity").AsBoolean().Nullable() - .WithColumn("StringData").AsString().Nullable() - .WithColumn("BinaryData").AsBinary().Nullable() + .WithColumn("StringData").AsString(MaxValue).Nullable() + .WithColumn("BinaryData").AsBinary(MaxValue).Nullable() .WithColumn("CreatedAt").AsDateTime2().NotNullable() .WithColumn("Version").AsInt32().NotNullable() .WithColumn("IsLatest").AsBoolean().NotNullable() @@ -61,7 +62,7 @@ public class Initial : Migration .WithColumn("DefinitionId").AsString().NotNullable() .WithColumn("DefinitionVersionId").AsString().NotNullable() .WithColumn("Version").AsInt32().NotNullable() - .WithColumn("WorkflowState").AsString().NotNullable() + .WithColumn("WorkflowState").AsString(MaxValue).NotNullable() .WithColumn("Status").AsString().NotNullable() .WithColumn("SubStatus").AsString().NotNullable() .WithColumn("CorrelationId").AsString().Nullable() @@ -78,7 +79,7 @@ public class Initial : Migration .WithColumn("DefinitionId").AsString().NotNullable() .WithColumn("DefinitionVersionId").AsString().NotNullable() .WithColumn("Version").AsInt32().NotNullable() - .WithColumn("WorkflowState").AsString().NotNullable() + .WithColumn("WorkflowState").AsString(MaxValue).NotNullable() .WithColumn("Status").AsString().NotNullable() .WithColumn("SubStatus").AsString().NotNullable() .WithColumn("CorrelationId").AsString().Nullable() diff --git a/src/modules/Elsa.Dapper.Migrations/Runtime/Initial.cs b/src/modules/Elsa.Dapper.Migrations/Runtime/Initial.cs index 04fe01593..221838fbd 100644 --- a/src/modules/Elsa.Dapper.Migrations/Runtime/Initial.cs +++ b/src/modules/Elsa.Dapper.Migrations/Runtime/Initial.cs @@ -1,5 +1,6 @@ using FluentMigrator; using JetBrains.Annotations; +using static System.Int32; namespace Elsa.Dapper.Migrations.Runtime; @@ -8,6 +9,7 @@ namespace Elsa.Dapper.Migrations.Runtime; [PublicAPI] public class Initial : Migration { + private const int MaxNodeIdColumnLength = 2048; /// public override void Up() { @@ -19,7 +21,7 @@ public class Initial : Migration .WithColumn("Name").AsString().NotNullable().Indexed() .WithColumn("ActivityId").AsString().NotNullable().Indexed() .WithColumn("Hash").AsString().Nullable().Indexed() - .WithColumn("SerializedPayload").AsString().Nullable(); + .WithColumn("SerializedPayload").AsString(MaxValue).Nullable(); IfDatabase("SqlServer", "Oracle", "MySql", "Postgres") .Create @@ -30,8 +32,8 @@ public class Initial : Migration .WithColumn("CorrelationId").AsString().Nullable().Indexed() .WithColumn("ActivityInstanceId").AsString().NotNullable().Indexed() .WithColumn("Hash").AsString().Nullable().Indexed() - .WithColumn("SerializedPayload").AsString().Nullable() - .WithColumn("SerializedMetadata").AsString().Nullable() + .WithColumn("SerializedPayload").AsString(MaxValue).Nullable() + .WithColumn("SerializedMetadata").AsString(MaxValue).Nullable() .WithColumn("CreatedAt").AsDateTimeOffset().NotNullable().Indexed(); IfDatabase("Sqlite") @@ -43,8 +45,8 @@ public class Initial : Migration .WithColumn("CorrelationId").AsString().Nullable().Indexed() .WithColumn("ActivityInstanceId").AsString().NotNullable().Indexed() .WithColumn("Hash").AsString().Nullable().Indexed() - .WithColumn("SerializedPayload").AsString().Nullable() - .WithColumn("SerializedMetadata").AsString().Nullable() + .WithColumn("SerializedPayload").AsString(MaxValue).Nullable() + .WithColumn("SerializedMetadata").AsString(MaxValue).Nullable() .WithColumn("CreatedAt").AsDateTime2().NotNullable().Indexed(); IfDatabase("SqlServer", "Oracle", "MySql", "Postgres") @@ -61,13 +63,13 @@ public class Initial : Migration .WithColumn("ActivityType").AsString().NotNullable().Indexed() .WithColumn("ActivityTypeVersion").AsInt32().NotNullable().Indexed() .WithColumn("ActivityName").AsString().Nullable().Indexed() - .WithColumn("ActivityNodeId").AsString().NotNullable().Indexed() + .WithColumn("ActivityNodeId").AsString(MaxNodeIdColumnLength).NotNullable().Indexed() .WithColumn("EventName").AsString().Nullable().Indexed() - .WithColumn("Message").AsString().Nullable() + .WithColumn("Message").AsString(MaxValue).Nullable() .WithColumn("Source").AsString().Nullable() - .WithColumn("SerializedActivityState").AsString().Nullable() - .WithColumn("SerializedPayload").AsString().Nullable() - .WithColumn("SerializedException").AsString().Nullable() + .WithColumn("SerializedActivityState").AsString(MaxValue).Nullable() + .WithColumn("SerializedPayload").AsString(MaxValue).Nullable() + .WithColumn("SerializedException").AsString(MaxValue).Nullable() .WithColumn("Timestamp").AsDateTimeOffset().NotNullable().Indexed() .WithColumn("Sequence").AsInt64().NotNullable().Indexed() ; @@ -86,14 +88,14 @@ public class Initial : Migration .WithColumn("ActivityType").AsString().NotNullable().Indexed() .WithColumn("ActivityTypeVersion").AsInt32().NotNullable().Indexed() .WithColumn("ActivityName").AsString().Nullable().Indexed() - .WithColumn("ActivityNodeId").AsString().NotNullable().Indexed() + .WithColumn("ActivityNodeId").AsString(MaxNodeIdColumnLength).NotNullable().Indexed() .WithColumn("EventName").AsString().Nullable().Indexed() - .WithColumn("Message").AsString().Nullable() + .WithColumn("Message").AsString(MaxValue).Nullable() .WithColumn("Source").AsString().Nullable() - .WithColumn("SerializedActivityState").AsString().Nullable() - .WithColumn("SerializedPayload").AsString().Nullable() - .WithColumn("SerializedOutputs").AsString().Nullable() - .WithColumn("SerializedException").AsString().Nullable() + .WithColumn("SerializedActivityState").AsString(MaxValue).Nullable() + .WithColumn("SerializedPayload").AsString(MaxValue).Nullable() + .WithColumn("SerializedOutputs").AsString(MaxValue).Nullable() + .WithColumn("SerializedException").AsString(MaxValue).Nullable() .WithColumn("Timestamp").AsDateTime2().NotNullable().Indexed() .WithColumn("Sequence").AsInt64().NotNullable().Indexed() ; @@ -104,14 +106,14 @@ public class Initial : Migration .WithColumn("Id").AsString().PrimaryKey() .WithColumn("WorkflowInstanceId").AsString().NotNullable().Indexed() .WithColumn("ActivityId").AsString().NotNullable().Indexed() - .WithColumn("ActivityNodeId").AsString().NotNullable().Indexed() + .WithColumn("ActivityNodeId").AsString(MaxNodeIdColumnLength).NotNullable().Indexed() .WithColumn("ActivityType").AsString().NotNullable().Indexed() .WithColumn("ActivityTypeVersion").AsInt32().NotNullable().Indexed() .WithColumn("ActivityName").AsString().Nullable().Indexed() - .WithColumn("SerializedActivityState").AsString().Nullable() - .WithColumn("SerializedPayload").AsString().Nullable() - .WithColumn("SerializedOutputs").AsString().Nullable() - .WithColumn("SerializedException").AsString().Nullable() + .WithColumn("SerializedActivityState").AsString(MaxValue).Nullable() + .WithColumn("SerializedPayload").AsString(MaxValue).Nullable() + .WithColumn("SerializedOutputs").AsString(MaxValue).Nullable() + .WithColumn("SerializedException").AsString(MaxValue).Nullable() .WithColumn("StartedAt").AsDateTimeOffset().NotNullable().Indexed() .WithColumn("CompletedAt").AsDateTimeOffset().Nullable().Indexed() .WithColumn("HasBookmarks").AsBoolean().NotNullable().Indexed() @@ -124,14 +126,14 @@ public class Initial : Migration .WithColumn("Id").AsString().PrimaryKey() .WithColumn("WorkflowInstanceId").AsString().NotNullable().Indexed() .WithColumn("ActivityId").AsString().NotNullable().Indexed() - .WithColumn("ActivityNodeId").AsString().NotNullable().Indexed() + .WithColumn("ActivityNodeId").AsString(MaxNodeIdColumnLength).NotNullable().Indexed() .WithColumn("ActivityType").AsString().NotNullable().Indexed() .WithColumn("ActivityTypeVersion").AsInt32().NotNullable().Indexed() .WithColumn("ActivityName").AsString().Nullable().Indexed() - .WithColumn("SerializedActivityState").AsString().Nullable() - .WithColumn("SerializedPayload").AsString().Nullable() - .WithColumn("SerializedOutputs").AsString().Nullable() - .WithColumn("SerializedException").AsString().Nullable() + .WithColumn("SerializedActivityState").AsString(MaxValue).Nullable() + .WithColumn("SerializedPayload").AsString(MaxValue).Nullable() + .WithColumn("SerializedOutputs").AsString(MaxValue).Nullable() + .WithColumn("SerializedException").AsString(MaxValue).Nullable() .WithColumn("StartedAt").AsDateTime2().NotNullable().Indexed() .WithColumn("CompletedAt").AsDateTime2().Nullable().Indexed() .WithColumn("HasBookmarks").AsBoolean().NotNullable().Indexed() @@ -147,8 +149,8 @@ public class Initial : Migration .WithColumn("ActivityInstanceId").AsString().Nullable().Indexed() .WithColumn("CorrelationId").AsString().Nullable().Indexed() .WithColumn("Hash").AsString().NotNullable().Indexed() - .WithColumn("SerializedBookmarkPayload").AsString() - .WithColumn("SerializedInput").AsString().Nullable() + .WithColumn("SerializedBookmarkPayload").AsString(MaxValue) + .WithColumn("SerializedInput").AsString(MaxValue).Nullable() .WithColumn("CreatedAt").AsDateTimeOffset().Indexed() .WithColumn("ExpiresAt").AsDateTimeOffset().Indexed() ; @@ -162,8 +164,8 @@ public class Initial : Migration .WithColumn("ActivityInstanceId").AsString().Nullable().Indexed() .WithColumn("CorrelationId").AsString().Nullable().Indexed() .WithColumn("Hash").AsString().NotNullable().Indexed() - .WithColumn("SerializedBookmarkPayload").AsString() - .WithColumn("SerializedInput").AsString().Nullable() + .WithColumn("SerializedBookmarkPayload").AsString(MaxValue) + .WithColumn("SerializedInput").AsString(MaxValue).Nullable() .WithColumn("CreatedAt").AsDateTime2().Indexed() .WithColumn("ExpiresAt").AsDateTime2().Indexed() ; diff --git a/src/modules/Elsa.Dapper.Migrations/Runtime/V3_1.cs b/src/modules/Elsa.Dapper.Migrations/Runtime/V3_1.cs index 3cefe7ac2..e453a8b41 100644 --- a/src/modules/Elsa.Dapper.Migrations/Runtime/V3_1.cs +++ b/src/modules/Elsa.Dapper.Migrations/Runtime/V3_1.cs @@ -1,11 +1,14 @@ +using System.Diagnostics.CodeAnalysis; using FluentMigrator; using JetBrains.Annotations; +using static System.Int32; namespace Elsa.Dapper.Migrations.Runtime; /// [Migration(20002, "Elsa:Runtime:AddKeyValueStore")] [PublicAPI] +[SuppressMessage("ReSharper", "InconsistentNaming")] public class V3_1 : Migration { /// @@ -14,7 +17,7 @@ public class V3_1 : Migration Create .Table("KeyValuePairs") .WithColumn("Key").AsString().PrimaryKey() - .WithColumn("Value").AsString().NotNullable(); + .WithColumn("Value").AsString(MaxValue).NotNullable(); } /// diff --git a/src/modules/Elsa.Dapper/Abstractions/SqlDialectBase.cs b/src/modules/Elsa.Dapper/Abstractions/SqlDialectBase.cs index f95831fa6..e14b57c5f 100644 --- a/src/modules/Elsa.Dapper/Abstractions/SqlDialectBase.cs +++ b/src/modules/Elsa.Dapper/Abstractions/SqlDialectBase.cs @@ -1,4 +1,6 @@ +using System.Text; using Elsa.Common.Entities; +using Elsa.Common.Models; using Elsa.Dapper.Contracts; namespace Elsa.Dapper.Abstractions; @@ -53,10 +55,25 @@ public abstract class SqlDialectBase : ISqlDialect } /// - public virtual string Skip(int count) => $"offset {count}"; + public virtual string Skip(int count) => $"offset {count} rows"; /// - public virtual string Take(int count) => $"limit {count}"; + public virtual string Take(int count) => $"fetch next {count} rows only"; + + /// + public virtual string Page(PageArgs pageArgs) + { + var sb = new StringBuilder(); + + // Attention: the order is important here for SQL Server (OFFSET before FETCH NEXT). + if (pageArgs.Offset != null) + sb.AppendLine(Skip(pageArgs.Offset.Value)); + + if (pageArgs.Limit != null) + sb.AppendLine(Take(pageArgs.Limit.Value)); + + return sb.ToString(); + } /// public string Insert(string table, string[] fields, Func? getParamName = default) @@ -75,6 +92,15 @@ public abstract class SqlDialectBase : ISqlDialect var fieldList = string.Join(", ", fields); var fieldParamNames = fields.Select(x => $"@{getParamName(x)}"); var fieldParamList = string.Join(", ", fieldParamNames); - return $"INSERT OR REPLACE INTO {table} ({primaryKeyField}, {fieldList}) VALUES (@{getParamName(primaryKeyField)}, {fieldParamList});"; + return @$" + MERGE INTO {table} WITH (HOLDLOCK) AS Target + USING (VALUES (@{getParamName(primaryKeyField)}, {fieldParamList})) + AS Source ({primaryKeyField}, {fieldList}) + ON Target.{primaryKeyField} = Source.{primaryKeyField} + WHEN MATCHED THEN + UPDATE SET {string.Join(", ", fields.Select(x => $"{x} = Source.{x}"))} + WHEN NOT MATCHED THEN + INSERT ({primaryKeyField}, {fieldList}) + VALUES (Source.{primaryKeyField}, {string.Join(", ", fields.Select(x => $"Source.{x}"))});"; } } \ No newline at end of file diff --git a/src/modules/Elsa.Dapper/Contracts/ISqlDialect.cs b/src/modules/Elsa.Dapper/Contracts/ISqlDialect.cs index 505451383..c83f4fc53 100644 --- a/src/modules/Elsa.Dapper/Contracts/ISqlDialect.cs +++ b/src/modules/Elsa.Dapper/Contracts/ISqlDialect.cs @@ -1,4 +1,5 @@ using Elsa.Common.Entities; +using Elsa.Common.Models; namespace Elsa.Dapper.Contracts; @@ -96,6 +97,13 @@ public interface ISqlDialect /// The number of records to take. string Take(int count); + /// + /// Adds pagination to a SQL query. + /// + /// The pagination arguments. + /// The modified SQL query with pagination. + string Page(PageArgs pageArgs); + /// /// Builds an INSERT query. /// diff --git a/src/modules/Elsa.Dapper/Dialects/SqlServerDialect.cs b/src/modules/Elsa.Dapper/Dialects/SqlServerDialect.cs index 4c4760a9f..665de65c5 100644 --- a/src/modules/Elsa.Dapper/Dialects/SqlServerDialect.cs +++ b/src/modules/Elsa.Dapper/Dialects/SqlServerDialect.cs @@ -8,8 +8,8 @@ namespace Elsa.Dapper.Dialects; public class SqlServerDialect : SqlDialectBase { /// - public override string Skip(int count) => $"OFFSET {count} ROWS"; + public override string Skip(int count) => $"Offset {count} Rows"; /// - public override string Take(int count) => $"FETCH NEXT {count} ROWS ONLY"; + public override string Take(int count) => $"fetch next {count} rows only"; } \ No newline at end of file diff --git a/src/modules/Elsa.Dapper/Dialects/SqliteDialect.cs b/src/modules/Elsa.Dapper/Dialects/SqliteDialect.cs index d4d1caf0a..f852ec22b 100644 --- a/src/modules/Elsa.Dapper/Dialects/SqliteDialect.cs +++ b/src/modules/Elsa.Dapper/Dialects/SqliteDialect.cs @@ -1,3 +1,5 @@ +using System.Text; +using Elsa.Common.Models; using Elsa.Dapper.Abstractions; namespace Elsa.Dapper.Dialects; @@ -7,4 +9,33 @@ namespace Elsa.Dapper.Dialects; /// public class SqliteDialect : SqlDialectBase { + /// + public override string Skip(int count) => $"offset {count}"; + + /// + public override string Take(int count) => $"limit {count}"; + + /// + public override string Page(PageArgs pageArgs) + { + var sb = new StringBuilder(); + + // Attention: the order is important here for SQLite (LIMIT must come before OFFSET). + if (pageArgs.Limit != null) + sb.AppendLine(Take(pageArgs.Limit.Value)); + + if (pageArgs.Offset != null) + sb.AppendLine(Skip(pageArgs.Offset.Value)); + + return sb.ToString(); + } + + public override string Upsert(string table, string primaryKeyField, string[] fields, Func? getParamName = default) + { + getParamName ??= x => x; + var fieldList = string.Join(", ", fields); + var fieldParamNames = fields.Select(x => $"@{getParamName(x)}"); + var fieldParamList = string.Join(", ", fieldParamNames); + return $"INSERT OR REPLACE INTO {table} ({primaryKeyField}, {fieldList}) VALUES (@{getParamName(primaryKeyField)}, {fieldParamList});"; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Dapper/Extensions/ParameterizedQueryBuilderExtensions.cs b/src/modules/Elsa.Dapper/Extensions/ParameterizedQueryBuilderExtensions.cs index ed20ce54a..e6dec0a26 100644 --- a/src/modules/Elsa.Dapper/Extensions/ParameterizedQueryBuilderExtensions.cs +++ b/src/modules/Elsa.Dapper/Extensions/ParameterizedQueryBuilderExtensions.cs @@ -1,3 +1,4 @@ +using System.Data; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using Elsa.Common.Entities; @@ -75,11 +76,12 @@ public static class ParameterizedQueryBuilderExtensions /// /// The query. /// The table. + /// The primary key. /// The inner query. - public static ParameterizedQuery Delete(this ParameterizedQuery query, string table, ParameterizedQuery innerQuery) + public static ParameterizedQuery Delete(this ParameterizedQuery query, string table, string primaryKey, ParameterizedQuery innerQuery) { query.Sql.AppendLine(query.Dialect.Delete(table)); - query.Sql.AppendLine("and rowid in ("); + query.Sql.AppendLine($"and {primaryKey} in ("); query.Sql.AppendLine(innerQuery.Sql.ToString()); query.Sql.AppendLine(")"); return query; @@ -324,13 +326,7 @@ public static class ParameterizedQueryBuilderExtensions /// The page arguments. public static ParameterizedQuery Page(this ParameterizedQuery query, PageArgs pageArgs) { - // Attention: the order is important here for SQLite (LIMIT must come before OFFSET). - if (pageArgs.Limit != null) - query.Take(pageArgs.Limit.Value); - - if (pageArgs.Offset != null) - query.Skip(pageArgs.Offset.Value); - + query.Sql.AppendLine(query.Dialect.Page(pageArgs)); return query; } @@ -350,16 +346,20 @@ public static class ParameterizedQueryBuilderExtensions .ToArray(); getParameterName ??= x => x; - + query.Sql.AppendLine(query.Dialect.Upsert(table, primaryKeyField, fields, getParameterName)); - + var primaryKeyValue = record.GetType().GetProperty(primaryKeyField)?.GetValue(record); query.Parameters.Add($"@{getParameterName(primaryKeyField)}", primaryKeyValue); - + + var recordType = record.GetType(); foreach (var field in fields) { - var value = record.GetType().GetProperty(field)?.GetValue(record); - query.Parameters.Add($"@{getParameterName(field)}", value); + var prop = recordType.GetProperty(field)!; + var propType = prop.PropertyType; + var value = prop.GetValue(record); + var dbType = value == null ? GetDbType(propType) : default; + query.Parameters.Add($"@{getParameterName(field)}", value, dbType); } return query; @@ -389,4 +389,10 @@ public static class ParameterizedQueryBuilderExtensions return query; } + + private static DbType? GetDbType(Type type) + { + if (type == typeof(byte[])) return DbType.Binary; + return null; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Dapper/Features/DapperFeature.cs b/src/modules/Elsa.Dapper/Features/DapperFeature.cs index 90194cdd5..a9bb0c4fa 100644 --- a/src/modules/Elsa.Dapper/Features/DapperFeature.cs +++ b/src/modules/Elsa.Dapper/Features/DapperFeature.cs @@ -17,9 +17,7 @@ public class DapperFeature : FeatureBase public DapperFeature(IModule module) : base(module) { // See: https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/dapper-limitations#data-types - SqlMapper.AddTypeHandler(new DateTimeOffsetHandler()); SqlMapper.AddTypeHandler(new GuidHandler()); - SqlMapper.AddTypeHandler(new TimeSpanHandler()); } /// diff --git a/src/modules/Elsa.Dapper/Features/DapperMigrationsFeature.cs b/src/modules/Elsa.Dapper/Features/DapperMigrationsFeature.cs index 45856bc84..9cee0c44f 100644 --- a/src/modules/Elsa.Dapper/Features/DapperMigrationsFeature.cs +++ b/src/modules/Elsa.Dapper/Features/DapperMigrationsFeature.cs @@ -18,11 +18,28 @@ public class DapperMigrationsFeature : FeatureBase { } + /// + /// Configures migrations to use SQLite. + /// + public DapperMigrationsFeature UseSqlite() + { + ConfigureRunner += runner => runner.AddSQLite(); + return this; + } + + /// + /// Configures migrations to use SQLite. + /// + public DapperMigrationsFeature UseSqlServer() + { + ConfigureRunner += runner => runner.AddSqlServer(); + return this; + } + /// /// Gets or sets a delegate to configure migrations. /// public Action ConfigureRunner { get; set; } = runner => runner - .AddSQLite() .WithGlobalConnectionString(sp => sp.GetRequiredService().GetConnectionString()) .WithMigrationsIn(typeof(Initial).Assembly); diff --git a/src/modules/Elsa.Dapper/Services/SqliteDbConnectionProvider.cs b/src/modules/Elsa.Dapper/Services/SqliteDbConnectionProvider.cs index 61dfb5fce..26e4aa73b 100644 --- a/src/modules/Elsa.Dapper/Services/SqliteDbConnectionProvider.cs +++ b/src/modules/Elsa.Dapper/Services/SqliteDbConnectionProvider.cs @@ -1,6 +1,8 @@ using System.Data; +using Dapper; using Elsa.Dapper.Contracts; using Elsa.Dapper.Dialects; +using Elsa.Dapper.TypeHandlers.Sqlite; using JetBrains.Annotations; using Microsoft.Data.Sqlite; @@ -12,7 +14,14 @@ namespace Elsa.Dapper.Services; [PublicAPI] public class SqliteDbConnectionProvider : IDbConnectionProvider { - private readonly string _connectionString = "Data Source=elsa.dapper.db"; + private readonly string _connectionString = "Data Source=:memory:;Cache=Shared"; + + static SqliteDbConnectionProvider() + { + // See: https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/dapper-limitations#data-types + SqlMapper.AddTypeHandler(new DateTimeOffsetHandler()); + SqlMapper.AddTypeHandler(new TimeSpanHandler()); + } /// /// Initializes a new instance of the class. @@ -29,9 +38,9 @@ public class SqliteDbConnectionProvider : IDbConnectionProvider { _connectionString = connectionString; } - + /// - public string GetConnectionString() =>_connectionString; + public string GetConnectionString() => _connectionString; /// public IDbConnection GetConnection() diff --git a/src/modules/Elsa.Dapper/Services/Store.cs b/src/modules/Elsa.Dapper/Services/Store.cs index eb6fde73c..0526cae05 100644 --- a/src/modules/Elsa.Dapper/Services/Store.cs +++ b/src/modules/Elsa.Dapper/Services/Store.cs @@ -264,7 +264,7 @@ public class Store where T : notnull filter(selectQuery); selectQuery = selectQuery.OrderBy(orderFields.ToArray()).Page(pageArgs); - var deleteQuery = _dbConnectionProvider.CreateQuery().Delete(TableName, selectQuery); + var deleteQuery = _dbConnectionProvider.CreateQuery().Delete(TableName, primaryKey, selectQuery); using var connection = _dbConnectionProvider.GetConnection(); return await deleteQuery.ExecuteAsync(connection); }