From 9f7bd6ecbcba1007b2339a6e1ba3905b2e7c323b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 11 Jul 2025 22:09:07 +0200 Subject: [PATCH] Refactor database extensions and support migrations for V3.6 Remove `DatabaseFacadeExtensions` and introduce `IWorkflowReferenceQuery` with its default implementation. Implement database schema updates for PostgreSQL, MySQL, and Oracle to enhance compatibility with the V3.6 data structure. --- Directory.Build.props | 2 +- .../Elsa.Studio.Agents.csproj | 5 +- .../Stores/DapperWorkflowDefinitionStore.cs | 1 + .../DesignTimeDbContextFactoryBase.cs | 9 +- .../Elsa.Persistence.EFCore.Common.csproj | 1 - .../ElsaDbContextBase.cs | 8 +- .../DbContextFactories.cs | 7 + .../Handlers/SetupForMySql.cs | 27 ++ .../20250711172853_V3_6.Designer.cs | 232 ++++++++++++++++++ .../Management/20250711172853_V3_6.cs | 50 ++++ .../ManagementElsaDbContextModelSnapshot.cs | 4 +- .../MySqlProvidersExtensions.cs | 3 + .../Services/MySqlWorkflowReferenceQuery.cs | 52 ++++ .../Configurations/Management.cs | 2 +- .../DbContextFactories.cs | 3 +- .../20250711191819_V3_6.Designer.cs | 232 ++++++++++++++++++ .../Management/20250711191819_V3_6.cs | 146 +++++++++++ .../ManagementElsaDbContextModelSnapshot.cs | 14 +- .../OracleProvidersExtensions.cs | 2 +- .../Services/OracleWorkflowReferenceQuery.cs | 44 ++++ .../DbContextFactories.cs | 7 + .../Extensions/DatabaseFacadeExtensions.cs | 10 - .../Handlers/SetupForPostgreSql.cs | 28 +++ .../Helpers/TableNameHelpers.cs | 19 ++ .../20250711092046_V3_6.Designer.cs | 232 ++++++++++++++++++ .../Management/20250711092046_V3_6.cs | 42 ++++ .../ManagementElsaDbContextModelSnapshot.cs | 4 +- .../PostgreSqlProvidersExtensions.cs | 3 + .../PostgreSqlWorkflowReferenceQuery.cs | 45 ++++ .../SqlServerWorkflowReferenceQuery.cs | 31 +++ .../Services/SqliteWorkflowReferenceQuery.cs | 31 +++ .../SqliteProvidersExtensions.cs | 1 - .../Elsa.Persistence.EFCore/efcore-3.6.sh | 24 ++ 33 files changed, 1286 insertions(+), 35 deletions(-) create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.MySql/Handlers/SetupForMySql.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/20250711172853_V3_6.Designer.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/20250711172853_V3_6.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.MySql/Services/MySqlWorkflowReferenceQuery.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/20250711191819_V3_6.Designer.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/20250711191819_V3_6.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Services/OracleWorkflowReferenceQuery.cs rename src/modules/persistence/{Elsa.Persistence.EFCore.Common => Elsa.Persistence.EFCore.PostgreSql}/Extensions/DatabaseFacadeExtensions.cs (53%) create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Handlers/SetupForPostgreSql.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Helpers/TableNameHelpers.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/20250711092046_V3_6.Designer.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/20250711092046_V3_6.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Services/PostgreSqlWorkflowReferenceQuery.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.SqlServer/Services/SqlServerWorkflowReferenceQuery.cs create mode 100644 src/modules/persistence/Elsa.Persistence.EFCore.Sqlite/Services/SqliteWorkflowReferenceQuery.cs create mode 100755 src/modules/persistence/Elsa.Persistence.EFCore/efcore-3.6.sh diff --git a/Directory.Build.props b/Directory.Build.props index 46090883..3a18f1c3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - false + true Elsa Workflows Community diff --git a/src/modules/agents/Elsa.Studio.Agents/Elsa.Studio.Agents.csproj b/src/modules/agents/Elsa.Studio.Agents/Elsa.Studio.Agents.csproj index 6fc682ae..7fdc025f 100644 --- a/src/modules/agents/Elsa.Studio.Agents/Elsa.Studio.Agents.csproj +++ b/src/modules/agents/Elsa.Studio.Agents/Elsa.Studio.Agents.csproj @@ -3,13 +3,14 @@ Provides a UI for managing agents. elsa extension studio module agent ai llm semantic kernel + true - + - + diff --git a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Management/Stores/DapperWorkflowDefinitionStore.cs b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Management/Stores/DapperWorkflowDefinitionStore.cs index b4ec891b..c2b1210f 100644 --- a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Management/Stores/DapperWorkflowDefinitionStore.cs +++ b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Management/Stores/DapperWorkflowDefinitionStore.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Elsa.Common.Entities; using Elsa.Common.Models; using Elsa.Persistence.Dapper.Extensions; diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs index 44f7c512..da0ab2a7 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Common/Abstractions/DesignTimeDbContextFactoryBase.cs @@ -23,12 +23,19 @@ public TDbContext CreateDbContext(string[] args) var parser = new Parser(command); var parseResult = parser.Parse(args); var connectionString = parseResult.GetValueForOption(connectionStringOption) ?? "Data Source=local"; - var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var services = new ServiceCollection(); + ConfigureServices(services); ConfigureBuilder(builder, connectionString); + var serviceProvider = services.BuildServiceProvider(); return (TDbContext)ActivatorUtilities.CreateInstance(serviceProvider, typeof(TDbContext), builder.Options); } + + protected virtual void ConfigureServices(IServiceCollection services) + { + // This method can be overridden to configure additional services if needed. + } /// /// Implement this to configure the . diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Common/Elsa.Persistence.EFCore.Common.csproj b/src/modules/persistence/Elsa.Persistence.EFCore.Common/Elsa.Persistence.EFCore.Common.csproj index 0e8260cb..a05ed51d 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.Common/Elsa.Persistence.EFCore.Common.csproj +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Common/Elsa.Persistence.EFCore.Common.csproj @@ -5,7 +5,6 @@ Provides common Entity Framework Core types for implementing EF Core persistence of varipus ELsa modules. elsa extension module persistence efcore - Elsa.EFCore diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Common/ElsaDbContextBase.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Common/ElsaDbContextBase.cs index fd3b29d3..ce6e6d29 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.Common/ElsaDbContextBase.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Common/ElsaDbContextBase.cs @@ -19,7 +19,7 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema }; protected IServiceProvider ServiceProvider { get; } - private readonly ElsaDbContextOptions? elsaDbContextOptions; + private readonly ElsaDbContextOptions? _elsaDbContextOptions; public string? TenantId { get; set; } /// @@ -41,10 +41,10 @@ public abstract class ElsaDbContextBase : DbContext, IElsaDbContextSchema protected ElsaDbContextBase(DbContextOptions options, IServiceProvider serviceProvider) : base(options) { ServiceProvider = serviceProvider; - elsaDbContextOptions = options.FindExtension()?.Options; + _elsaDbContextOptions = options.FindExtension()?.Options; // ReSharper disable once VirtualMemberCallInConstructor - Schema = !string.IsNullOrWhiteSpace(elsaDbContextOptions?.SchemaName) ? elsaDbContextOptions.SchemaName : ElsaSchema; + Schema = !string.IsNullOrWhiteSpace(_elsaDbContextOptions?.SchemaName) ? _elsaDbContextOptions.SchemaName : ElsaSchema; var tenantAccessor = serviceProvider.GetService(); var tenantId = tenantAccessor?.Tenant?.Id; @@ -66,7 +66,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) if (!string.IsNullOrWhiteSpace(Schema)) modelBuilder.HasDefaultSchema(Schema); - var additionalConfigurations = elsaDbContextOptions?.GetModelConfigurations(this); + var additionalConfigurations = _elsaDbContextOptions?.GetModelConfigurations(this); additionalConfigurations?.Invoke(modelBuilder); diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/DbContextFactories.cs b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/DbContextFactories.cs index d720b895..911b4663 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/DbContextFactories.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/DbContextFactories.cs @@ -6,8 +6,10 @@ using Elsa.Persistence.EFCore.Modules.Management; using Elsa.Persistence.EFCore.Modules.Runtime; using Elsa.Persistence.EFCore.Modules.Tenants; +using Elsa.Persistence.EFCore.MySql.Handlers; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -33,6 +35,11 @@ public class TenantsDbContextFactories : MySqlDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase where TDbContext : DbContext { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + } + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) { builder.UseElsaMySql(GetType().Assembly, connectionString, serverVersion: ServerVersion.Parse("9.0.0")); diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Handlers/SetupForMySql.cs b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Handlers/SetupForMySql.cs new file mode 100644 index 00000000..9537b66a --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Handlers/SetupForMySql.cs @@ -0,0 +1,27 @@ +using Elsa.Workflows.Management.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Elsa.Persistence.EFCore.MySql.Handlers; + +/// +/// Represents a class that handles entity model creation for SQLite databases. +/// +public class SetupForMySql : IEntityModelCreatingHandler +{ + /// + public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) + { + if (!dbContext.Database.IsMySql()) + return; + + // Configure the WorkflowDefinition entity to use the PostgreSQL JSONB type for the StringData property: + if (entityType.ClrType == typeof(WorkflowDefinition)) + { + modelBuilder + .Entity(entityType.Name) + .Property("StringData") + .HasColumnType("JSON"); + } + } +} \ No newline at end of file diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/20250711172853_V3_6.Designer.cs b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/20250711172853_V3_6.Designer.cs new file mode 100644 index 00000000..0416ba12 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/20250711172853_V3_6.Designer.cs @@ -0,0 +1,232 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Management; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Persistence.EFCore.MySql.Migrations.Management +{ + [DbContext(typeof(ManagementElsaDbContext))] + [Migration("20250711172853_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowDefinition", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("BinaryData") + .HasColumnType("longblob"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DefinitionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsLatest") + .HasColumnType("tinyint(1)"); + + b.Property("IsPublished") + .HasColumnType("tinyint(1)"); + + b.Property("IsReadonly") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("MaterializerContext") + .HasColumnType("longtext"); + + b.Property("MaterializerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("ProviderName") + .HasColumnType("longtext"); + + b.Property("StringData") + .HasColumnType("JSON"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("ToolVersion") + .HasColumnType("longtext"); + + b.Property("UsableAsActivity") + .HasColumnType("tinyint(1)"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IsLatest") + .HasDatabaseName("IX_WorkflowDefinition_IsLatest"); + + b.HasIndex("IsPublished") + .HasDatabaseName("IX_WorkflowDefinition_IsPublished"); + + b.HasIndex("IsSystem") + .HasDatabaseName("IX_WorkflowDefinition_IsSystem"); + + b.HasIndex("Name") + .HasDatabaseName("IX_WorkflowDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowDefinition_TenantId"); + + b.HasIndex("UsableAsActivity") + .HasDatabaseName("IX_WorkflowDefinition_UsableAsActivity"); + + b.HasIndex("Version") + .HasDatabaseName("IX_WorkflowDefinition_Version"); + + b.HasIndex("DefinitionId", "Version") + .IsUnique() + .HasDatabaseName("IX_WorkflowDefinition_DefinitionId_Version"); + + b.ToTable("WorkflowDefinitions", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowInstance", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("CorrelationId") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DataCompressionAlgorithm") + .HasColumnType("longtext"); + + b.Property("DefinitionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("DefinitionVersionId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FinishedAt") + .HasColumnType("datetime(6)"); + + b.Property("IncidentCount") + .HasColumnType("int"); + + b.Property("IsExecuting") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("ParentWorkflowInstanceId") + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("SubStatus") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("IX_WorkflowInstance_CorrelationId"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_WorkflowInstance_CreatedAt"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("IX_WorkflowInstance_DefinitionId"); + + b.HasIndex("FinishedAt") + .HasDatabaseName("IX_WorkflowInstance_FinishedAt"); + + b.HasIndex("IsExecuting") + .HasDatabaseName("IX_WorkflowInstance_IsExecuting"); + + b.HasIndex("IsSystem") + .HasDatabaseName("IX_WorkflowInstance_IsSystem"); + + b.HasIndex("Name") + .HasDatabaseName("IX_WorkflowInstance_Name"); + + b.HasIndex("Status") + .HasDatabaseName("IX_WorkflowInstance_Status"); + + b.HasIndex("SubStatus") + .HasDatabaseName("IX_WorkflowInstance_SubStatus"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowInstance_TenantId"); + + b.HasIndex("UpdatedAt") + .HasDatabaseName("IX_WorkflowInstance_UpdatedAt"); + + b.HasIndex("Status", "DefinitionId") + .HasDatabaseName("IX_WorkflowInstance_Status_DefinitionId"); + + b.HasIndex("Status", "SubStatus") + .HasDatabaseName("IX_WorkflowInstance_Status_SubStatus"); + + b.HasIndex("SubStatus", "DefinitionId") + .HasDatabaseName("IX_WorkflowInstance_SubStatus_DefinitionId"); + + b.HasIndex("Status", "SubStatus", "DefinitionId", "Version") + .HasDatabaseName("IX_WorkflowInstance_Status_SubStatus_DefinitionId_Version"); + + b.ToTable("WorkflowInstances", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/20250711172853_V3_6.cs b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/20250711172853_V3_6.cs new file mode 100644 index 00000000..637f3177 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/20250711172853_V3_6.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.MySql.Migrations.Management +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "StringData", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "JSON", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "StringData", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "JSON", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs index 0402a61e..6c509e7a 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("ProductVersion", "9.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -70,7 +70,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("longtext"); b.Property("StringData") - .HasColumnType("longtext"); + .HasColumnType("JSON"); b.Property("TenantId") .HasColumnType("varchar(255)"); diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/MySqlProvidersExtensions.cs b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/MySqlProvidersExtensions.cs index 6cea2edc..77e7a720 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/MySqlProvidersExtensions.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/MySqlProvidersExtensions.cs @@ -1,4 +1,6 @@ using System.Reflection; +using Elsa.Extensions; +using Elsa.Persistence.EFCore.MySql.Handlers; using Microsoft.EntityFrameworkCore.Infrastructure; // ReSharper disable once CheckNamespace @@ -53,6 +55,7 @@ public static TFeature UseMySql(this PersistenceFeatureBas where TDbContext : ElsaDbContextBase where TFeature : PersistenceFeatureBase { + feature.Module.Services.TryAddScopedImplementation(); feature.DbContextOptionsBuilder = (sp, db) => db.UseElsaMySql(migrationsAssembly, connectionStringFunc(sp), options, configure: configure); return (TFeature)feature; } diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Services/MySqlWorkflowReferenceQuery.cs b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Services/MySqlWorkflowReferenceQuery.cs new file mode 100644 index 00000000..06d93b21 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.MySql/Services/MySqlWorkflowReferenceQuery.cs @@ -0,0 +1,52 @@ +using Elsa.Persistence.EFCore.Modules.Management; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; +using Microsoft.EntityFrameworkCore; +using MySqlConnector; + +namespace Elsa.Persistence.EFCore.MySql.Services; + +/// +/// Provides an implementation of for querying MySQL databases +/// to find all latest workflow definitions that reference a specific workflow definition. +/// +public class MySqlWorkflowReferenceQuery(IDbContextFactory dbContextFactory) : IWorkflowReferenceQuery +{ + public async Task> ExecuteAsync(string workflowDefinitionId, CancellationToken cancellationToken = default) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var tableName = dbContext.Model.FindEntityType(typeof(WorkflowDefinition))!.GetTableName(); + + var sql = $""" + SELECT + t.DefinitionId, + t.TenantId + FROM {tableName} AS t + WHERE + t.IsLatest = 1 + AND EXISTS ( + SELECT 1 + FROM {tableName} AS t2 + CROSS JOIN JSON_TABLE( + t2.StringData, + '$.activities[*]' + COLUMNS ( + workflowDefinitionId VARCHAR(255) PATH '$.workflowDefinitionId', + latestAvailablePublishedVersion INT PATH '$.latestAvailablePublishedVersion' + ) + ) AS act + WHERE + t2.DefinitionId = t.DefinitionId + AND act.workflowDefinitionId = @p0 + AND act.latestAvailablePublishedVersion IS NOT NULL + ) + """; + + var param = new MySqlParameter("@p0", workflowDefinitionId); + + return await dbContext.Set() + .FromSqlRaw(sql, param) + .Select(x => x.DefinitionId) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Configurations/Management.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Configurations/Management.cs index a6847fb6..a5a9beec 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Configurations/Management.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Configurations/Management.cs @@ -10,7 +10,7 @@ public void Configure(EntityTypeBuilder builder) { // In order to use data more than 2000 char we have to use NCLOB. // In Oracle, we have to explicitly say the column is NCLOB otherwise it would be considered nvarchar(2000). - builder.Property("StringData").HasColumnType("NCLOB"); + builder.Property("StringData").HasColumnType("JSON"); builder.Property("Data").HasColumnType("NCLOB"); builder.Property(x => x.Description).HasColumnType("NCLOB"); builder.Property(x => x.MaterializerContext).HasColumnType("NCLOB"); diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/DbContextFactories.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/DbContextFactories.cs index be4c8420..d5940df3 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/DbContextFactories.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/DbContextFactories.cs @@ -35,7 +35,6 @@ public class OracleDesignTimeDbContextFactory : DesignTimeDbContextF { protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) { - var options = new ElsaDbContextOptions().Configure(); - builder.UseElsaOracle(GetType().Assembly, connectionString, options); + builder.UseElsaOracle(GetType().Assembly, connectionString); } } \ No newline at end of file diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/20250711191819_V3_6.Designer.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/20250711191819_V3_6.Designer.cs new file mode 100644 index 00000000..f558c90e --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/20250711191819_V3_6.Designer.cs @@ -0,0 +1,232 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Management; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Oracle.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management +{ + [DbContext(typeof(ManagementElsaDbContext))] + [Migration("20250711191819_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowDefinition", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("BinaryData") + .HasColumnType("RAW(2000)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Data") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("DefinitionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Description") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("IsLatest") + .HasColumnType("BOOLEAN"); + + b.Property("IsPublished") + .HasColumnType("BOOLEAN"); + + b.Property("IsReadonly") + .HasColumnType("BOOLEAN"); + + b.Property("IsSystem") + .HasColumnType("BOOLEAN"); + + b.Property("MaterializerContext") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("MaterializerName") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Name") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ProviderName") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("StringData") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ToolVersion") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("UsableAsActivity") + .HasColumnType("BOOLEAN"); + + b.Property("Version") + .HasColumnType("NUMBER(10)"); + + b.HasKey("Id"); + + b.HasIndex("IsLatest") + .HasDatabaseName("IX_WorkflowDefinition_IsLatest"); + + b.HasIndex("IsPublished") + .HasDatabaseName("IX_WorkflowDefinition_IsPublished"); + + b.HasIndex("IsSystem") + .HasDatabaseName("IX_WorkflowDefinition_IsSystem"); + + b.HasIndex("Name") + .HasDatabaseName("IX_WorkflowDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowDefinition_TenantId"); + + b.HasIndex("UsableAsActivity") + .HasDatabaseName("IX_WorkflowDefinition_UsableAsActivity"); + + b.HasIndex("Version") + .HasDatabaseName("IX_WorkflowDefinition_Version"); + + b.HasIndex("DefinitionId", "Version") + .IsUnique() + .HasDatabaseName("IX_WorkflowDefinition_DefinitionId_Version"); + + b.ToTable("WorkflowDefinitions", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowInstance", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CorrelationId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Data") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("DataCompressionAlgorithm") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("DefinitionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("DefinitionVersionId") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("FinishedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("IncidentCount") + .HasColumnType("NUMBER(10)"); + + b.Property("IsExecuting") + .HasColumnType("BOOLEAN"); + + b.Property("IsSystem") + .HasColumnType("BOOLEAN"); + + b.Property("Name") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ParentWorkflowInstanceId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("SubStatus") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("UpdatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Version") + .HasColumnType("NUMBER(10)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("IX_WorkflowInstance_CorrelationId"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_WorkflowInstance_CreatedAt"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("IX_WorkflowInstance_DefinitionId"); + + b.HasIndex("FinishedAt") + .HasDatabaseName("IX_WorkflowInstance_FinishedAt"); + + b.HasIndex("IsExecuting") + .HasDatabaseName("IX_WorkflowInstance_IsExecuting"); + + b.HasIndex("IsSystem") + .HasDatabaseName("IX_WorkflowInstance_IsSystem"); + + b.HasIndex("Name") + .HasDatabaseName("IX_WorkflowInstance_Name"); + + b.HasIndex("Status") + .HasDatabaseName("IX_WorkflowInstance_Status"); + + b.HasIndex("SubStatus") + .HasDatabaseName("IX_WorkflowInstance_SubStatus"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowInstance_TenantId"); + + b.HasIndex("UpdatedAt") + .HasDatabaseName("IX_WorkflowInstance_UpdatedAt"); + + b.HasIndex("Status", "DefinitionId") + .HasDatabaseName("IX_WorkflowInstance_Status_DefinitionId"); + + b.HasIndex("Status", "SubStatus") + .HasDatabaseName("IX_WorkflowInstance_Status_SubStatus"); + + b.HasIndex("SubStatus", "DefinitionId") + .HasDatabaseName("IX_WorkflowInstance_SubStatus_DefinitionId"); + + b.HasIndex("Status", "SubStatus", "DefinitionId", "Version") + .HasDatabaseName("IX_WorkflowInstance_Status_SubStatus_DefinitionId_Version"); + + b.ToTable("WorkflowInstances", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/20250711191819_V3_6.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/20250711191819_V3_6.cs new file mode 100644 index 00000000..13bd85f2 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/20250711191819_V3_6.cs @@ -0,0 +1,146 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Oracle.Migrations.Management +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Data", + schema: _schema.Schema, + table: "WorkflowInstances", + type: "NVARCHAR2(2000)", + nullable: true, + oldClrType: typeof(string), + oldType: "NCLOB", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "StringData", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "NVARCHAR2(2000)", + nullable: true, + oldClrType: typeof(string), + oldType: "NCLOB", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MaterializerContext", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "NVARCHAR2(2000)", + nullable: true, + oldClrType: typeof(string), + oldType: "NCLOB", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Description", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "NVARCHAR2(2000)", + nullable: true, + oldClrType: typeof(string), + oldType: "NCLOB", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Data", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "NVARCHAR2(2000)", + nullable: true, + oldClrType: typeof(string), + oldType: "NCLOB", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "BinaryData", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "RAW(2000)", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "BLOB", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Data", + schema: _schema.Schema, + table: "WorkflowInstances", + type: "NCLOB", + nullable: true, + oldClrType: typeof(string), + oldType: "NVARCHAR2(2000)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "StringData", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "NCLOB", + nullable: true, + oldClrType: typeof(string), + oldType: "NVARCHAR2(2000)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MaterializerContext", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "NCLOB", + nullable: true, + oldClrType: typeof(string), + oldType: "NVARCHAR2(2000)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Description", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "NCLOB", + nullable: true, + oldClrType: typeof(string), + oldType: "NVARCHAR2(2000)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Data", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "NCLOB", + nullable: true, + oldClrType: typeof(string), + oldType: "NVARCHAR2(2000)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "BinaryData", + schema: _schema.Schema, + table: "WorkflowDefinitions", + type: "BLOB", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "RAW(2000)", + oldNullable: true); + } + } +} diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs index d9279489..64491cfb 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("ProductVersion", "9.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 128); OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -29,20 +29,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("NVARCHAR2(450)"); b.Property("BinaryData") - .HasColumnType("BLOB"); + .HasColumnType("RAW(2000)"); b.Property("CreatedAt") .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); b.Property("Data") - .HasColumnType("NCLOB"); + .HasColumnType("NVARCHAR2(2000)"); b.Property("DefinitionId") .IsRequired() .HasColumnType("NVARCHAR2(450)"); b.Property("Description") - .HasColumnType("NCLOB"); + .HasColumnType("NVARCHAR2(2000)"); b.Property("IsLatest") .HasColumnType("BOOLEAN"); @@ -57,7 +57,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("BOOLEAN"); b.Property("MaterializerContext") - .HasColumnType("NCLOB"); + .HasColumnType("NVARCHAR2(2000)"); b.Property("MaterializerName") .IsRequired() @@ -70,7 +70,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("NVARCHAR2(2000)"); b.Property("StringData") - .HasColumnType("NCLOB"); + .HasColumnType("NVARCHAR2(2000)"); b.Property("TenantId") .HasColumnType("NVARCHAR2(450)"); @@ -126,7 +126,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); b.Property("Data") - .HasColumnType("NCLOB"); + .HasColumnType("NVARCHAR2(2000)"); b.Property("DataCompressionAlgorithm") .HasColumnType("NVARCHAR2(2000)"); diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/OracleProvidersExtensions.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/OracleProvidersExtensions.cs index b96ea723..71a32173 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/OracleProvidersExtensions.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/OracleProvidersExtensions.cs @@ -65,7 +65,7 @@ public static TFeature UseOracle(this PersistenceFeatureBa feature.DbContextOptionsBuilder = (sp, db) => db.UseElsaOracle(migrationsAssembly, connectionStringFunc(sp), options, configure: configure); return (TFeature)feature; } - + public static ElsaDbContextOptions Configure(this ElsaDbContextOptions options) { var management = new Management(); diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Services/OracleWorkflowReferenceQuery.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Services/OracleWorkflowReferenceQuery.cs new file mode 100644 index 00000000..7d6e634f --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Oracle/Services/OracleWorkflowReferenceQuery.cs @@ -0,0 +1,44 @@ +using Elsa.Persistence.EFCore.Modules.Management; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Persistence.EFCore.Oracle.Services; + +/// +/// Provides an implementation of for querying Oracle databases +/// to find all latest workflow definitions that reference a specific workflow definition. +/// +public class OracleWorkflowReferenceQuery(IDbContextFactory dbContextFactory) : IWorkflowReferenceQuery +{ + public async Task> ExecuteAsync(string workflowDefinitionId, CancellationToken cancellationToken = default) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var tableName = dbContext.Model.FindEntityType(typeof(WorkflowDefinition))!.GetSchemaQualifiedTableName(); + + var sql = $""" + SELECT "DefinitionId", "TenantId" + FROM {tableName} + WHERE "IsLatest" = 1 + AND EXISTS ( + SELECT 1 + FROM JSON_TABLE( + "StringData", + '$.activities[*]' + COLUMNS ( + workflowDefinitionId VARCHAR2(255) PATH '$.workflowDefinitionId', + latestAvailablePublishedVersion VARCHAR2(4000) FORMAT JSON PATH '$.latestAvailablePublishedVersion' + ) + ) act + WHERE act.workflowDefinitionId = :p0 + AND act.latestAvailablePublishedVersion IS NOT NULL + ) + """; + + + return await dbContext.Set() + .FromSqlRaw(sql, workflowDefinitionId) + .Select(x => x.DefinitionId) + .ToListAsync(cancellationToken); + } +} diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/DbContextFactories.cs b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/DbContextFactories.cs index 5b88401d..ef1232a3 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/DbContextFactories.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/DbContextFactories.cs @@ -6,8 +6,10 @@ using Elsa.Persistence.EFCore.Modules.Management; using Elsa.Persistence.EFCore.Modules.Runtime; using Elsa.Persistence.EFCore.Modules.Tenants; +using Elsa.Persistence.EFCore.PostgreSql.Handlers; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -33,6 +35,11 @@ public class TenantsDbContextFactories : PostgreSqlDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase where TDbContext : DbContext { + protected override void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + } + protected override void ConfigureBuilder(DbContextOptionsBuilder builder, string connectionString) { builder.UseElsaPostgreSql(GetType().Assembly, connectionString); diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Common/Extensions/DatabaseFacadeExtensions.cs b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Extensions/DatabaseFacadeExtensions.cs similarity index 53% rename from src/modules/persistence/Elsa.Persistence.EFCore.Common/Extensions/DatabaseFacadeExtensions.cs rename to src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Extensions/DatabaseFacadeExtensions.cs index 54f13cf6..f28125c2 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.Common/Extensions/DatabaseFacadeExtensions.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Extensions/DatabaseFacadeExtensions.cs @@ -8,16 +8,6 @@ namespace Elsa.Persistence.EFCore.Extensions; /// public static class DatabaseFacadeExtensions { - /// - /// Returns true if the database provider is MySql. - /// - public static bool IsMySql(this DatabaseFacade database) => database.ProviderName == "Pomelo.EntityFrameworkCore.MySql"; - - /// - /// Returns true if the database provider is Oracle. - /// - public static bool IsOracle(this DatabaseFacade database) => database.ProviderName == "Oracle.EntityFrameworkCore"; - /// /// Returns true if the database provider is Postgres. /// diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Handlers/SetupForPostgreSql.cs b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Handlers/SetupForPostgreSql.cs new file mode 100644 index 00000000..241d1422 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Handlers/SetupForPostgreSql.cs @@ -0,0 +1,28 @@ +using Elsa.Persistence.EFCore.Extensions; +using Elsa.Workflows.Management.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Elsa.Persistence.EFCore.PostgreSql.Handlers; + +/// +/// Represents a class that handles entity model creation for SQLite databases. +/// +public class SetupForPostgreSql : IEntityModelCreatingHandler +{ + /// + public void Handle(ElsaDbContextBase dbContext, ModelBuilder modelBuilder, IMutableEntityType entityType) + { + if (!dbContext.Database.IsPostgres()) + return; + + // Configure the WorkflowDefinition entity to use the PostgreSQL JSONB type for the StringData property: + if (entityType.ClrType == typeof(WorkflowDefinition)) + { + modelBuilder + .Entity(entityType.Name) + .Property("StringData") + .HasColumnType("jsonb"); + } + } +} \ No newline at end of file diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Helpers/TableNameHelpers.cs b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Helpers/TableNameHelpers.cs new file mode 100644 index 00000000..a94b923c --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Helpers/TableNameHelpers.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Elsa.Persistence.EFCore.PostgreSql.Helpers; + +public static class TableNameHelpers +{ + public static string QuoteSchemaQualifiedTableName(this IEntityType entityType) + { + var schemaQualifiedTableName = entityType.GetSchemaQualifiedTableName()!; + return QuoteSchemaQualifiedTableName(schemaQualifiedTableName); + } + + public static string QuoteSchemaQualifiedTableName(string schemaQualifiedTableName) + { + var parts = schemaQualifiedTableName.Split('.'); + return string.Join(".", parts.Select(p => $"\"{p}\"")); + } +} \ No newline at end of file diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/20250711092046_V3_6.Designer.cs b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/20250711092046_V3_6.Designer.cs new file mode 100644 index 00000000..4af821b5 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/20250711092046_V3_6.Designer.cs @@ -0,0 +1,232 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Management; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Management +{ + [DbContext(typeof(ManagementElsaDbContext))] + [Migration("20250711092046_V3_6")] + partial class V3_6 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowDefinition", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BinaryData") + .HasColumnType("bytea"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DefinitionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsLatest") + .HasColumnType("boolean"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("IsReadonly") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("MaterializerContext") + .HasColumnType("text"); + + b.Property("MaterializerName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("ProviderName") + .HasColumnType("text"); + + b.Property("StringData") + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("ToolVersion") + .HasColumnType("text"); + + b.Property("UsableAsActivity") + .HasColumnType("boolean"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsLatest") + .HasDatabaseName("IX_WorkflowDefinition_IsLatest"); + + b.HasIndex("IsPublished") + .HasDatabaseName("IX_WorkflowDefinition_IsPublished"); + + b.HasIndex("IsSystem") + .HasDatabaseName("IX_WorkflowDefinition_IsSystem"); + + b.HasIndex("Name") + .HasDatabaseName("IX_WorkflowDefinition_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowDefinition_TenantId"); + + b.HasIndex("UsableAsActivity") + .HasDatabaseName("IX_WorkflowDefinition_UsableAsActivity"); + + b.HasIndex("Version") + .HasDatabaseName("IX_WorkflowDefinition_Version"); + + b.HasIndex("DefinitionId", "Version") + .IsUnique() + .HasDatabaseName("IX_WorkflowDefinition_DefinitionId_Version"); + + b.ToTable("WorkflowDefinitions", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Management.Entities.WorkflowInstance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DataCompressionAlgorithm") + .HasColumnType("text"); + + b.Property("DefinitionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DefinitionVersionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IncidentCount") + .HasColumnType("integer"); + + b.Property("IsExecuting") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("ParentWorkflowInstanceId") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("SubStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId") + .HasDatabaseName("IX_WorkflowInstance_CorrelationId"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_WorkflowInstance_CreatedAt"); + + b.HasIndex("DefinitionId") + .HasDatabaseName("IX_WorkflowInstance_DefinitionId"); + + b.HasIndex("FinishedAt") + .HasDatabaseName("IX_WorkflowInstance_FinishedAt"); + + b.HasIndex("IsExecuting") + .HasDatabaseName("IX_WorkflowInstance_IsExecuting"); + + b.HasIndex("IsSystem") + .HasDatabaseName("IX_WorkflowInstance_IsSystem"); + + b.HasIndex("Name") + .HasDatabaseName("IX_WorkflowInstance_Name"); + + b.HasIndex("Status") + .HasDatabaseName("IX_WorkflowInstance_Status"); + + b.HasIndex("SubStatus") + .HasDatabaseName("IX_WorkflowInstance_SubStatus"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowInstance_TenantId"); + + b.HasIndex("UpdatedAt") + .HasDatabaseName("IX_WorkflowInstance_UpdatedAt"); + + b.HasIndex("Status", "DefinitionId") + .HasDatabaseName("IX_WorkflowInstance_Status_DefinitionId"); + + b.HasIndex("Status", "SubStatus") + .HasDatabaseName("IX_WorkflowInstance_Status_SubStatus"); + + b.HasIndex("SubStatus", "DefinitionId") + .HasDatabaseName("IX_WorkflowInstance_SubStatus_DefinitionId"); + + b.HasIndex("Status", "SubStatus", "DefinitionId", "Version") + .HasDatabaseName("IX_WorkflowInstance_Status_SubStatus_DefinitionId_Version"); + + b.ToTable("WorkflowInstances", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/20250711092046_V3_6.cs b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/20250711092046_V3_6.cs new file mode 100644 index 00000000..f69d9b92 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/20250711092046_V3_6.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Management +{ + /// + public partial class V3_6 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_6(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + var schema = _schema.Schema; + migrationBuilder.Sql($@" + ALTER TABLE ""{schema}"".""WorkflowDefinitions"" + ALTER COLUMN ""StringData"" + TYPE jsonb + USING ""StringData""::jsonb; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + var schema = _schema.Schema; + migrationBuilder.Sql($@" + ALTER TABLE ""{schema}"".""WorkflowDefinitions"" + ALTER COLUMN ""StringData"" + TYPE text + USING ""StringData""::text; + "); + } + } +} \ No newline at end of file diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs index 5e4dd90f..fcbcdd06 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Migrations/Management/ManagementElsaDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("Elsa") - .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("ProductVersion", "9.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -70,7 +70,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("StringData") - .HasColumnType("text"); + .HasColumnType("jsonb"); b.Property("TenantId") .HasColumnType("text"); diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/PostgreSqlProvidersExtensions.cs b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/PostgreSqlProvidersExtensions.cs index 4531be0c..63683ac4 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/PostgreSqlProvidersExtensions.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/PostgreSqlProvidersExtensions.cs @@ -1,4 +1,6 @@ using System.Reflection; +using Elsa.Extensions; +using Elsa.Persistence.EFCore.PostgreSql.Handlers; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; // ReSharper disable once CheckNamespace @@ -50,6 +52,7 @@ public static TFeature UsePostgreSql(this PersistenceFeatu where TDbContext : ElsaDbContextBase where TFeature : PersistenceFeatureBase { + feature.Module.Services.TryAddScopedImplementation(); feature.DbContextOptionsBuilder = (sp, db) => db.UseElsaPostgreSql(migrationsAssembly, connectionStringFunc(sp), options, configure: configure); return (TFeature)feature; } diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Services/PostgreSqlWorkflowReferenceQuery.cs b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Services/PostgreSqlWorkflowReferenceQuery.cs new file mode 100644 index 00000000..ddb69b9a --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.PostgreSql/Services/PostgreSqlWorkflowReferenceQuery.cs @@ -0,0 +1,45 @@ +using Elsa.Persistence.EFCore.Modules.Management; +using Elsa.Persistence.EFCore.PostgreSql.Helpers; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Persistence.EFCore.PostgreSql.Services; + +/// +/// Provides an implementation of for querying PostgreSQL databases +/// to find all latest workflow definitions that reference a specific workflow definition. +/// +public class PostgreSqlWorkflowReferenceQuery(IDbContextFactory dbContextFactory) : IWorkflowReferenceQuery +{ + public async Task> ExecuteAsync(string workflowDefinitionId, CancellationToken cancellationToken = default) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var tableName = dbContext.Model.FindEntityType(typeof(WorkflowDefinition))!.QuoteSchemaQualifiedTableName(); + + var sql = $""" + SELECT `DefinitionId`, `TenantId` + FROM {tableName} + WHERE `IsLatest` = 1 + AND EXISTS ( + SELECT 1 + FROM JSON_TABLE( + `StringData`, + '$.activities[*]' + COLUMNS ( + workflowDefinitionId VARCHAR(255) PATH '$.workflowDefinitionId', + latestAvailablePublishedVersion JSON PATH '$.latestAvailablePublishedVersion' + ) + ) AS act + WHERE act.workflowDefinitionId = @p0 + AND act.latestAvailablePublishedVersion IS NOT NULL + ); + """; + + + return await dbContext.Set() + .FromSqlRaw(sql, workflowDefinitionId) + .Select(x => x.DefinitionId) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.SqlServer/Services/SqlServerWorkflowReferenceQuery.cs b/src/modules/persistence/Elsa.Persistence.EFCore.SqlServer/Services/SqlServerWorkflowReferenceQuery.cs new file mode 100644 index 00000000..3a32878c --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.SqlServer/Services/SqlServerWorkflowReferenceQuery.cs @@ -0,0 +1,31 @@ +using Elsa.Persistence.EFCore.Modules.Management; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Persistence.EFCore.SqlServer.Services; + +/// +/// Provides an implementation of for querying SQL Server databases +/// to find all latest workflow definitions that reference a specific workflow definition. +/// +public class SqlServerWorkflowReferenceQuery(IDbContextFactory dbContextFactory) : IWorkflowReferenceQuery +{ + public async Task> ExecuteAsync(string workflowDefinitionId, CancellationToken cancellationToken = default) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var tableName = dbContext.Model.FindEntityType(typeof(WorkflowDefinition))!.GetSchemaQualifiedTableName(); + + var sql = $""" + SELECT [DefinitionId], [TenantId] FROM {tableName} WHERE [IsLatest] = 1 AND EXISTS ( + SELECT 1 FROM OPENJSON(JSON_QUERY([StringData], '$.activities')) AS value + WHERE JSON_VALUE(value, '$.workflowDefinitionId') = @p0 + AND JSON_VALUE(value, '$.latestAvailablePublishedVersion') IS NOT NULL) + """; + + return await dbContext.Set() + .FromSqlRaw(sql, workflowDefinitionId) + .Select(x => x.DefinitionId) + .ToListAsync(cancellationToken); + } +} diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Sqlite/Services/SqliteWorkflowReferenceQuery.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Sqlite/Services/SqliteWorkflowReferenceQuery.cs new file mode 100644 index 00000000..af7886d6 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Sqlite/Services/SqliteWorkflowReferenceQuery.cs @@ -0,0 +1,31 @@ +using Elsa.Persistence.EFCore.Modules.Management; +using Elsa.Workflows.Management; +using Elsa.Workflows.Management.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Elsa.Persistence.EFCore.Sqlite.Services; + +/// +/// Provides an implementation of for querying SQLite databases +/// to find all latest workflow definitions that reference a specific workflow definition. +/// +public class SqliteWorkflowReferenceQuery(IDbContextFactory dbContextFactory) : IWorkflowReferenceQuery +{ + public async Task> ExecuteAsync(string workflowDefinitionId, CancellationToken cancellationToken = default) + { + await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + var tableName = dbContext.Model.FindEntityType(typeof(WorkflowDefinition))!.GetTableName(); + + var sql = $""" + SELECT DefinitionId, TenantId FROM {tableName} WHERE IsLatest = 1 AND EXISTS ( + SELECT 1 FROM json_each(json_extract(StringData, '$.activities')) + WHERE json_extract(value, '$.workflowDefinitionId') = @p0 + AND json_extract(value, '$.latestAvailablePublishedVersion') IS NOT NULL) + """; + + return await dbContext.Set() + .FromSqlRaw(sql, workflowDefinitionId) + .Select(x => x.DefinitionId) + .ToListAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/modules/persistence/Elsa.Persistence.EFCore.Sqlite/SqliteProvidersExtensions.cs b/src/modules/persistence/Elsa.Persistence.EFCore.Sqlite/SqliteProvidersExtensions.cs index 5d6d1334..c4665a3b 100644 --- a/src/modules/persistence/Elsa.Persistence.EFCore.Sqlite/SqliteProvidersExtensions.cs +++ b/src/modules/persistence/Elsa.Persistence.EFCore.Sqlite/SqliteProvidersExtensions.cs @@ -68,7 +68,6 @@ public static TFeature UseSqlite(this PersistenceFeatureBa where TDbContext : ElsaDbContextBase where TFeature : PersistenceFeatureBase { - feature.Module.Services.TryAddScopedImplementation(); feature.DbContextOptionsBuilder = (sp, db) => db.UseElsaSqlite(migrationsAssembly, connectionStringFunc(sp), options, configure: configure); return (TFeature)feature; diff --git a/src/modules/persistence/Elsa.Persistence.EFCore/efcore-3.6.sh b/src/modules/persistence/Elsa.Persistence.EFCore/efcore-3.6.sh new file mode 100755 index 00000000..460644e8 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.EFCore/efcore-3.6.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Define the modules to update +mods=("Management") + +# Define the list of providers +# providers=("MySql" "SqlServer" "Sqlite" "PostgreSql" "Oracle") +providers=("Oracle") + +# Loop through each module +for module in "${mods[@]}"; do + # Loop through each provider + for provider in "${providers[@]}"; do + providerPath="../Elsa.Persistence.EFCore.$provider" + startupProject="$providerPath/Elsa.Persistence.EFCore.$provider.csproj" + migrationsPath="Migrations/$module" + + echo "Updating migrations for $provider..." + echo "Provider path: ${providerPath:?}" + echo "Startup project: $startupProject" + echo "Migrations path: $migrationsPath" + ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_6 -c ""$module""ElsaDbContext -p ""$providerPath"" -o ""$migrationsPath"" --startup-project ""$startupProject""" + done +done