diff --git a/Directory.Packages.props b/Directory.Packages.props index dd2e28eac..005c17183 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -49,6 +49,7 @@ + @@ -112,44 +113,44 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index 8801d54d9..30802d6a5 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -58,6 +58,7 @@ + diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 7cae86cb3..d065d535b 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -49,6 +49,12 @@ using Elsa.Workflows.Runtime.Distributed.Extensions; using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Stores; using Elsa.Workflows.Runtime.Tasks; +using Hangfire; +using Hangfire.MemoryStorage; +using Hangfire.PostgreSql; +using Hangfire.PostgreSql.Factories; +using Hangfire.SqlServer; +using Hangfire.Storage.SQLite; using JetBrains.Annotations; using Medallion.Threading.FileSystem; using Medallion.Threading.Postgres; @@ -78,8 +84,8 @@ const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed; const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit; const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; -const bool useMultitenancy = false; -const bool useTenantsFromConfiguration = false; +const bool useMultitenancy = true; +const bool useTenantsFromConfiguration = true; const bool useAgents = false; const bool useSecrets = false; const bool disableVariableWrappers = false; @@ -133,7 +139,36 @@ services }); if (useHangfire) - elsa.UseHangfire(); + { + JobStorage jobStorage; + if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) + { + jobStorage = new PostgreSqlStorage(new NpgsqlConnectionFactory(postgresConnectionString, new() + { + QueuePollInterval = TimeSpan.FromSeconds(1) + })); + } + else if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) + { + jobStorage = new SQLiteStorage(sqliteConnectionString, new() + { + QueuePollInterval = TimeSpan.FromSeconds(1) + }); + } + else if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) + { + jobStorage = new SqlServerStorage(sqlServerConnectionString, new() + { + QueuePollInterval = TimeSpan.FromSeconds(1) + }); + } + else + { + jobStorage = new MemoryStorage(); + } + + elsa.UseHangfire(hangfire => hangfire.UseJobStorage(jobStorage)); + } elsa .AddActivitiesFrom() diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index 66598c7b9..221cdcf22 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -2,14 +2,7 @@ "Logging": { "LogLevel": { "Default": "Warning", - "Elsa": "Warning", - "MassTransit": "Warning", - "Microsoft.Extensions.Http": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.EntityFrameworkCore": "Warning", - "Microsoft.AspNetCore": "Warning", - "Quartz": "Warning", - "System.Net.Http": "Warning" + "Microsoft.Hosting.Lifetime": "Information" } }, "HostBuilder": { diff --git a/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj b/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj index 8b940ae22..2474602ce 100644 --- a/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj +++ b/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj @@ -8,14 +8,14 @@ - - - + + + - - + + diff --git a/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs index ab493f267..b06060b46 100644 --- a/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs @@ -25,6 +25,7 @@ public static class ModuleExtensions /// /// Configures Hangfire to use SQL Server storage. Only use this feature if you are not configuring Hangfire yourself. /// + [Obsolete("Configure storage directly on the HangfireFeature.")] public static HangfireFeature UseSqlServerStorage(this HangfireFeature feature, Action configure) { feature.Module.Use(configure); @@ -34,6 +35,7 @@ public static class ModuleExtensions /// /// Configures Hangfire to use SQLite storage. Only use this feature if you are not configuring Hangfire yourself. /// + [Obsolete("Configure storage directly on the HangfireFeature.")] public static HangfireFeature UseSqliteStorage(this HangfireFeature feature, Action configure) { feature.Module.Use(configure); diff --git a/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs b/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs index 25c359cfe..0162ccd11 100644 --- a/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs +++ b/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs @@ -1,5 +1,7 @@ using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; using Elsa.Features.Services; +using Elsa.Workflows.Runtime.Features; using Hangfire; using Hangfire.MemoryStorage; using Newtonsoft.Json; @@ -9,35 +11,63 @@ namespace Elsa.Hangfire.Features; /// /// Sets up Hangfire. If you're setting up Hangfire yourself, then you should not enable this feature. /// -public class HangfireFeature : FeatureBase +[DependsOn(typeof(WorkflowRuntimeFeature))] // Ensure that the workflow runtime feature's hosted services have executed before Hangfire Server starts. +public class HangfireFeature(IModule module) : FeatureBase(module) { - /// - public HangfireFeature(IModule module) : base(module) - { - } - /// /// A delegate that configures Hangfire. /// - public Action ConfigureHangfire { get; set; } = (_, cfg) => cfg.UseMemoryStorage(); + private Action _configureHangfire = (_, _) => { }; /// /// A delegate that configures Hangfire's background job server options. /// - public Action ConfigureBackgroundServerOptions { get; set; } = (_, _) => { }; + private Action _configureBackgroundServerOptions = (_, _) => { }; /// /// A delegate that creates a job storage instance. /// - public Func CreateJobStorage { get; set; } = () => new MemoryStorage(); + private Func _createJobStorage = () => new MemoryStorage(); + + /// + /// Configures Hangfire. + /// + public HangfireFeature ConfigureHangfire(Action configure) + { + _configureHangfire += configure; + return this; + } + + /// + /// Configures Hangfire's background job server options. + /// + public HangfireFeature ConfigureBackgroundServerOptions(Action configure) + { + _configureBackgroundServerOptions += configure; + return this; + } + + public HangfireFeature UseMemoryStorage() + { + return UseJobStorage(new MemoryStorage()); + } + + public HangfireFeature UseJobStorage(JobStorage storage) + { + _createJobStorage = () => storage; + return this; + } /// public override void Apply() { + var jobStorage = _createJobStorage(); + Action configAction = (sp, cfg) => { cfg.UseSimpleAssemblyNameTypeSerializer(); cfg.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects); + cfg.UseStorage(jobStorage); }; Action serverOptionsAction = (sp, options) => @@ -46,10 +76,10 @@ public class HangfireFeature : FeatureBase options.SchedulePollingInterval = TimeSpan.FromSeconds(1); }; - configAction += ConfigureHangfire; - serverOptionsAction += ConfigureBackgroundServerOptions; + configAction += _configureHangfire; + serverOptionsAction += _configureBackgroundServerOptions; Services.AddHangfire(configAction); - Services.AddHangfireServer(serverOptionsAction, CreateJobStorage()); + Services.AddHangfireServer(serverOptionsAction, jobStorage); } } \ No newline at end of file diff --git a/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs b/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs index 480a5a3bf..fafb92528 100644 --- a/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs +++ b/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs @@ -11,18 +11,14 @@ namespace Elsa.Hangfire.Features; /// Configures the Hangfire feature to use SQL Server storage. If you're setting up Hangfire yourself, then you should not enable this feature. /// [DependsOn(typeof(HangfireFeature))] -public class HangfireSqlServerStorageFeature : FeatureBase +[Obsolete("Configure storage directly on the HangfireFeature.")] +public class HangfireSqlServerStorageFeature(IModule module) : FeatureBase(module) { - /// - public HangfireSqlServerStorageFeature(IModule module) : base(module) - { - } - /// /// The connection string to use when connecting to SQL Server, or the name of the connection string. /// - public string NameOrConnectionString { get; set; } = default!; - + public string NameOrConnectionString { get; set; } = null!; + /// /// Configures the SQL Server storage options. /// @@ -41,13 +37,11 @@ public class HangfireSqlServerStorageFeature : FeatureBase UseRecommendedIsolationLevel = true }; ConfigureSqlServerStorageOptions(storageOptions); - - hangfireFeature.ConfigureHangfire = (_, cfg) => + + hangfireFeature.ConfigureHangfire((_, cfg) => { cfg.UseSqlServerStorage(NameOrConnectionString, storageOptions); - }; - - hangfireFeature.CreateJobStorage = () => new SqlServerStorage(NameOrConnectionString, storageOptions); + }); }); } } \ No newline at end of file diff --git a/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs b/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs index 7982a078b..dcd7353df 100644 --- a/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs +++ b/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs @@ -10,17 +10,13 @@ namespace Elsa.Hangfire.Features; /// Configures the Hangfire feature to use SQLite storage. If you're setting up Hangfire yourself, then you should not enable this feature. /// [DependsOn(typeof(HangfireFeature))] -public class HangfireSqliteStorageFeature : FeatureBase +[Obsolete("Configure storage directly on the HangfireFeature.")] +public class HangfireSqliteStorageFeature(IModule module) : FeatureBase(module) { - /// - public HangfireSqliteStorageFeature(IModule module) : base(module) - { - } - /// /// The connection string to use when connecting to SQL Server, or the name of the connection string. /// - public string NameOrConnectionString { get; set; } = default!; + public string NameOrConnectionString { get; set; } = null!; /// /// Configures the SQL Server storage options. @@ -38,12 +34,10 @@ public class HangfireSqliteStorageFeature : FeatureBase }; ConfigureSqlServerStorageOptions(storageOptions); - hangfireFeature.ConfigureHangfire = (_, cfg) => + hangfireFeature.ConfigureHangfire((_, cfg) => { cfg.UseSQLiteStorage(NameOrConnectionString, storageOptions); - }; - - hangfireFeature.CreateJobStorage = () => new SQLiteStorage(NameOrConnectionString, storageOptions); + }); }); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index bd8623ce8..545ba3bbc 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -22,7 +22,7 @@ namespace Elsa.Workflows; /// The child being scheduled. /// The delegate to invoke when the scheduled activity completes. /// An optional tag. -public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = default); +public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = null); /// /// Provides context to the currently executing workflow. @@ -239,7 +239,7 @@ public partial class WorkflowExecutionContext : IExecutionContext public WorkflowSubStatus SubStatus { get; internal set; } /// The root associated with the execution context. - public MemoryRegister MemoryRegister { get; private set; } = default!; + public MemoryRegister MemoryRegister { get; private set; } = null!; /// A unique ID of the execution context. public string Id { get; set; } @@ -406,7 +406,7 @@ public partial class WorkflowExecutionContext : IExecutionContext /// /// Registers a completion callback for the specified activity. /// - internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = default, object? tag = default) + internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = null, object? tag = null) { var entry = new ActivityCompletionCallbackEntry(owner, child, completionCallback, tag); _completionCallbackEntries.Add(entry); @@ -420,7 +420,7 @@ public partial class WorkflowExecutionContext : IExecutionContext var entry = _completionCallbackEntries.FirstOrDefault(x => x.Owner == owner && x.Child == child); if (entry == null) - return default; + return null; RemoveCompletionCallback(entry); return entry; @@ -449,25 +449,25 @@ public partial class WorkflowExecutionContext : IExecutionContext ? FindActivityByInstanceId(handle.ActivityInstanceId) : handle.ActivityHash != null ? FindActivityByHash(handle.ActivityHash) - : default; + : null; } /// /// Returns the with the specified activity ID from the workflow graph. /// - public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : default; + public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : null; /// /// Returns the with the specified hash of the activity node ID from the workflow graph. /// /// The hash of the activity node ID. /// The with the specified hash of the activity node ID. - public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : default; + public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : null; /// Returns the containing the specified activity from the workflow graph. public ActivityNode? FindNodeByActivity(IActivity activity) { - return NodeActivityLookup.TryGetValue(activity, out var node) ? node : default; + return NodeActivityLookup.TryGetValue(activity, out var node) ? node : null; } /// Returns the associated with the specified activity ID. @@ -526,7 +526,7 @@ public partial class WorkflowExecutionContext : IExecutionContext } /// Creates a new for the specified activity. - public async Task CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = default) + public async Task CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = null) { var activityDescriptor = await ActivityRegistryLookup.FindAsync(activity) ?? throw new ActivityNotFoundException(activity.Type); var tag = options?.Tag; @@ -568,7 +568,7 @@ public partial class WorkflowExecutionContext : IExecutionContext public ActivityOutputRegister GetActivityOutputRegister() => TransientProperties.GetOrAdd(ActivityOutputRegistryKey, () => new ActivityOutputRegister()); /// Returns the last activity result. - public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : default; + public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : null; /// Adds the specified to the workflow execution context. public void AddActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Add(context);