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);