Add project files.

This commit is contained in:
2025-08-18 17:11:43 +03:30
parent 807dc263be
commit e5eb10d604
3943 changed files with 223719 additions and 0 deletions
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>
Provides Hangfire integration and an implementation of Elsa's IJobQueue implementation.
</Description>
<PackageTags>elsa module hangfire jobs scheduling</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Hangfire"/>
<PackageReference Include="Hangfire.MemoryStorage"/>
<PackageReference Include="Hangfire.Storage.SQLite"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj"/>
<ProjectReference Include="..\Elsa.Scheduling\Elsa.Scheduling.csproj"/>
</ItemGroup>
</Project>
@@ -0,0 +1,55 @@
using Elsa.Hangfire.Jobs;
using Hangfire;
using Hangfire.Storage.Monitoring;
namespace Elsa.Hangfire.Extensions;
/// <summary>
/// A set of extension methods for <see cref="JobStorage"/>.
/// </summary>
public static class JobStorageExtensions
{
/// <summary>
/// Enumerates all scheduled jobs of a given type.
/// </summary>
public static IEnumerable<KeyValuePair<string, ScheduledJobDto>> EnumerateScheduledJobs(this JobStorage storage, string name)
{
var api = storage.GetMonitoringApi();
var skip = 0;
const int take = 100;
JobList<ScheduledJobDto> scheduledJobs;
do
{
scheduledJobs = api.ScheduledJobs(skip, take);
var jobs = scheduledJobs.FindAll(x => x.Value.Job?.Type == typeof(RunWorkflowJob) || x.Value.Job?.Type == typeof(ResumeWorkflowJob));
foreach (var job in jobs.Where(x => (string)x.Value.Job.Args[0] == name))
yield return job;
skip += take;
} while (scheduledJobs.Count == take);
}
/// <summary>
/// Enumerates all enqueued jobs of a given type.
/// </summary>
public static IEnumerable<KeyValuePair<string, EnqueuedJobDto>> EnumerateQueuedJobs(this JobStorage storage, string queueName, string taskName)
{
var api = storage.GetMonitoringApi();
var skip = 0;
const int take = 100;
JobList<EnqueuedJobDto> enqueuedJobs;
do
{
enqueuedJobs = api.EnqueuedJobs(queueName, skip, take);
var jobs = enqueuedJobs.FindAll(x => x.Value.Job?.Type == typeof(RunWorkflowJob) || x.Value.Job?.Type == typeof(ResumeWorkflowJob));
foreach (var job in jobs.Where(x => (string)x.Value.Job.Args[0] == taskName))
yield return job;
skip += take;
} while (enqueuedJobs.Count == take);
}
}
@@ -0,0 +1,62 @@
using Elsa.Features.Services;
using Elsa.Hangfire.Features;
using Elsa.Scheduling;
using Elsa.Scheduling.Features;
using Elsa.Workflows.Runtime.Features;
using JetBrains.Annotations;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Provides extension methods for the <see cref="HangfireFeature"/>.
/// </summary>
[PublicAPI]
public static class ModuleExtensions
{
/// <summary>
/// Installs and configures Hangfire. Only use this feature if you are not configuring Hangfire yourself.
/// </summary>
public static IModule UseHangfire(this IModule module, Action<HangfireFeature>? configure = default)
{
return module.Use(configure);
}
/// <summary>
/// Configures Hangfire to use SQL Server storage. Only use this feature if you are not configuring Hangfire yourself.
/// </summary>
[Obsolete("Configure storage directly on the HangfireFeature.")]
public static HangfireFeature UseSqlServerStorage(this HangfireFeature feature, Action<HangfireSqlServerStorageFeature> configure)
{
feature.Module.Use(configure);
return feature;
}
/// <summary>
/// Configures Hangfire to use SQLite storage. Only use this feature if you are not configuring Hangfire yourself.
/// </summary>
[Obsolete("Configure storage directly on the HangfireFeature.")]
public static HangfireFeature UseSqliteStorage(this HangfireFeature feature, Action<HangfireSqliteStorageFeature> configure)
{
feature.Module.Use(configure);
return feature;
}
/// <summary>
/// Installs a Hangfire implementation for <see cref="IWorkflowScheduler"/>.
/// </summary>
public static SchedulingFeature UseHangfireScheduler(this SchedulingFeature feature, Action<HangfireSchedulerFeature>? configure = default)
{
feature.Module.Use(configure);
return feature;
}
/// <summary>
/// Installs a Hangfire implementation for <see cref="IWorkflowScheduler"/>.
/// </summary>
public static WorkflowRuntimeFeature UseHangfireBackgroundActivityScheduler(this WorkflowRuntimeFeature feature, Action<HangfireBackgroundActivitySchedulerFeature>? configure = default)
{
feature.Module.Use(configure);
return feature;
}
}
@@ -0,0 +1,23 @@
namespace Elsa.Hangfire.Extensions;
/// <summary>
/// Adds extension methods to <see cref="TimeSpan"/> that converts it to a cron expression.
/// </summary>
public static class TimeSpanExtensions
{
/// <summary>
/// Converts the specified time span to a cron expression.
/// </summary>
/// <param name="timeSpan">The time span.</param>
/// <returns>The cron expression.</returns>
public static string ToCronExpression(this TimeSpan timeSpan)
{
static string CreateCronComponent(int number) => (number > 0 ? $"*/{number}" : "*");
var cron = CreateCronComponent(timeSpan.Seconds);
cron += ' ' + CreateCronComponent(timeSpan.Minutes);
cron += ' ' + CreateCronComponent(timeSpan.Hours);
cron += ' ' + CreateCronComponent(timeSpan.Days);
return cron + " * *";
}
}
@@ -0,0 +1,36 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Hangfire.Services;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Features;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Hangfire.Features;
/// <summary>
/// Installs a Hangfire implementation for <see cref="IBackgroundActivityScheduler"/>.
/// </summary>
[DependsOn(typeof(WorkflowRuntimeFeature))]
public class HangfireBackgroundActivitySchedulerFeature : FeatureBase
{
/// <inheritdoc />
public HangfireBackgroundActivitySchedulerFeature(IModule module) : base(module)
{
}
/// <inheritdoc />
public override void Configure()
{
Module.Configure<WorkflowRuntimeFeature>(workflowRuntimeFeature =>
{
workflowRuntimeFeature.BackgroundActivityScheduler = sp => sp.GetRequiredService<HangfireBackgroundActivityScheduler>();
});
}
/// <inheritdoc />
public override void Apply()
{
Services.AddSingleton<HangfireBackgroundActivityScheduler>();
}
}
@@ -0,0 +1,85 @@
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;
namespace Elsa.Hangfire.Features;
/// <summary>
/// Sets up Hangfire. If you're setting up Hangfire yourself, then you should not enable this feature.
/// </summary>
[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)
{
/// <summary>
/// A delegate that configures Hangfire.
/// </summary>
private Action<IServiceProvider, IGlobalConfiguration> _configureHangfire = (_, _) => { };
/// <summary>
/// A delegate that configures Hangfire's background job server options.
/// </summary>
private Action<IServiceProvider, BackgroundJobServerOptions> _configureBackgroundServerOptions = (_, _) => { };
/// <summary>
/// A delegate that creates a job storage instance.
/// </summary>
private Func<JobStorage> _createJobStorage = () => new MemoryStorage();
/// <summary>
/// Configures Hangfire.
/// </summary>
public HangfireFeature ConfigureHangfire(Action<IServiceProvider, IGlobalConfiguration> configure)
{
_configureHangfire += configure;
return this;
}
/// <summary>
/// Configures Hangfire's background job server options.
/// </summary>
public HangfireFeature ConfigureBackgroundServerOptions(Action<IServiceProvider, BackgroundJobServerOptions> configure)
{
_configureBackgroundServerOptions += configure;
return this;
}
public HangfireFeature UseMemoryStorage()
{
return UseJobStorage(new MemoryStorage());
}
public HangfireFeature UseJobStorage(JobStorage storage)
{
_createJobStorage = () => storage;
return this;
}
/// <inheritdoc />
public override void Apply()
{
var jobStorage = _createJobStorage();
Action<IServiceProvider, IGlobalConfiguration> configAction = (sp, cfg) =>
{
cfg.UseSimpleAssemblyNameTypeSerializer();
cfg.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects);
cfg.UseStorage(jobStorage);
};
Action<IServiceProvider, BackgroundJobServerOptions> serverOptionsAction = (sp, options) =>
{
options.WorkerCount = 1;
options.SchedulePollingInterval = TimeSpan.FromSeconds(1);
};
configAction += _configureHangfire;
serverOptionsAction += _configureBackgroundServerOptions;
Services.AddHangfire(configAction);
Services.AddHangfireServer(serverOptionsAction, jobStorage);
}
}
@@ -0,0 +1,39 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Hangfire.Handlers;
using Elsa.Hangfire.Services;
using Elsa.Scheduling;
using Elsa.Scheduling.Features;
using Elsa.Workflows;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Hangfire.Features;
/// <summary>
/// Installs a Hangfire implementation for <see cref="IWorkflowScheduler"/>.
/// </summary>
[DependsOn(typeof(SchedulingFeature))]
public class HangfireSchedulerFeature : FeatureBase
{
/// <inheritdoc />
public HangfireSchedulerFeature(IModule module) : base(module)
{
}
/// <inheritdoc />
public override void Configure()
{
Module.Configure<SchedulingFeature>(schedulingFeature =>
{
schedulingFeature.WorkflowScheduler = sp => sp.GetRequiredService<HangfireWorkflowScheduler>();
});
}
/// <inheritdoc />
public override void Apply()
{
Services.AddSingleton<HangfireWorkflowScheduler>();
Services.AddSingleton<IActivityDescriptorModifier, CronActivityDescriptorModifier>();
}
}
@@ -0,0 +1,47 @@
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Hangfire;
using Hangfire.SqlServer;
namespace Elsa.Hangfire.Features;
/// <summary>
/// Configures the Hangfire feature to use SQL Server storage. If you're setting up Hangfire yourself, then you should not enable this feature.
/// </summary>
[DependsOn(typeof(HangfireFeature))]
[Obsolete("Configure storage directly on the HangfireFeature.")]
public class HangfireSqlServerStorageFeature(IModule module) : FeatureBase(module)
{
/// <summary>
/// The connection string to use when connecting to SQL Server, or the name of the connection string.
/// </summary>
public string NameOrConnectionString { get; set; } = null!;
/// <summary>
/// Configures the SQL Server storage options.
/// </summary>
public Action<SqlServerStorageOptions> ConfigureSqlServerStorageOptions { get; set; } = _ => { };
/// <inheritdoc />
public override void Configure()
{
Module.Use<HangfireFeature>(hangfireFeature =>
{
var storageOptions = new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.FromSeconds(15),
UseRecommendedIsolationLevel = true
};
ConfigureSqlServerStorageOptions(storageOptions);
hangfireFeature.ConfigureHangfire((_, cfg) =>
{
cfg.UseSqlServerStorage(NameOrConnectionString, storageOptions);
});
});
}
}
@@ -0,0 +1,43 @@
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Hangfire.Storage.SQLite;
namespace Elsa.Hangfire.Features;
/// <summary>
/// Configures the Hangfire feature to use SQLite storage. If you're setting up Hangfire yourself, then you should not enable this feature.
/// </summary>
[DependsOn(typeof(HangfireFeature))]
[Obsolete("Configure storage directly on the HangfireFeature.")]
public class HangfireSqliteStorageFeature(IModule module) : FeatureBase(module)
{
/// <summary>
/// The connection string to use when connecting to SQL Server, or the name of the connection string.
/// </summary>
public string NameOrConnectionString { get; set; } = null!;
/// <summary>
/// Configures the SQL Server storage options.
/// </summary>
public Action<SQLiteStorageOptions> ConfigureSqlServerStorageOptions { get; set; } = _ => { };
/// <inheritdoc />
public override void Configure()
{
Module.Use<HangfireFeature>(hangfireFeature =>
{
var storageOptions = new SQLiteStorageOptions
{
QueuePollInterval = TimeSpan.FromSeconds(1)
};
ConfigureSqlServerStorageOptions(storageOptions);
hangfireFeature.ConfigureHangfire((_, cfg) =>
{
cfg.UseSQLiteStorage(NameOrConnectionString, storageOptions);
});
});
}
}
@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait />
</Weavers>
@@ -0,0 +1,21 @@
using Elsa.Scheduling.Activities;
using Elsa.Workflows;
using Elsa.Workflows.Helpers;
using Elsa.Workflows.Models;
namespace Elsa.Hangfire.Handlers;
/// <summary>
/// Modifies the <see cref="ActivityDescriptor"/> of the <see cref="Cron"/> activity.
/// </summary>
public class CronActivityDescriptorModifier : IActivityDescriptorModifier
{
/// <inheritdoc />
public void Modify(ActivityDescriptor descriptor)
{
if (descriptor.TypeName != ActivityTypeNameHelper.GenerateTypeName<Cron>())
return;
descriptor.Description = "Schedules the execution of the activity using a cron expression using Hangfire.";
}
}
@@ -0,0 +1,32 @@
using Elsa.Common.Multitenancy;
using Elsa.Workflows.Runtime;
using JetBrains.Annotations;
namespace Elsa.Hangfire.Jobs;
/// <summary>
/// A job that executes a background activity.
/// </summary>
[UsedImplicitly]
public class ExecuteBackgroundActivityJob(IBackgroundActivityInvoker backgroundActivityInvoker, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor)
{
/// <summary>
/// Executes the job.
/// </summary>
public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, string? tenantId, CancellationToken cancellationToken = default)
{
var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null;
using var scope = tenantAccessor.PushContext(tenant);
await backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken);
}
/// <summary>
/// Executes the job.
/// </summary>
[Obsolete("Use the other overload.")]
[UsedImplicitly]
public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
{
await backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken);
}
}
@@ -0,0 +1,53 @@
using Elsa.Common.Multitenancy;
using Elsa.Scheduling;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using Hangfire;
using JetBrains.Annotations;
namespace Elsa.Hangfire.Jobs;
/// <summary>
/// A job that resumes a workflow.
/// </summary>
public class ResumeWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor)
{
/// <summary>
/// Executes the job.
/// </summary>
/// <param name="taskName">A unique name for this job.</param>
/// <param name="request">The workflow request.</param>
/// <param name="tenantId">The ID of the current tenant scheduling this job.</param>
/// <param name="cancellationToken">The cancellation token.</param>
// ReSharper disable once UnusedParameter.Global
[AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Fail)]
public async Task ExecuteAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken)
{
var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null;
using var scope = tenantAccessor.PushContext(tenant);
var client = await workflowRuntime.CreateClientAsync(request.WorkflowInstanceId, cancellationToken);
var runRequest = new RunWorkflowInstanceRequest
{
BookmarkId = request.BookmarkId,
ActivityHandle = request.ActivityHandle,
Input = request.Input,
Properties = request.Properties
};
await client.RunInstanceAsync(runRequest, cancellationToken);
}
[Obsolete("Use the other overload.")]
[UsedImplicitly]
public async Task ExecuteAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, CancellationToken cancellationToken)
{
var client = await workflowRuntime.CreateClientAsync(request.WorkflowInstanceId, cancellationToken);
var runRequest = new RunWorkflowInstanceRequest
{
BookmarkId = request.BookmarkId,
ActivityHandle = request.ActivityHandle,
Input = request.Input,
Properties = request.Properties
};
await client.RunInstanceAsync(runRequest, cancellationToken);
}
}
@@ -0,0 +1,57 @@
using Elsa.Common.Multitenancy;
using Elsa.Scheduling;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Messages;
using Hangfire;
using JetBrains.Annotations;
namespace Elsa.Hangfire.Jobs;
/// <summary>
/// A job that resumes a workflow.
/// </summary>
public class RunWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor)
{
/// <summary>
/// Executes the job.
/// </summary>
/// <param name="taskName">A unique name for this job.</param>
/// <param name="request">The workflow request.</param>
/// <param name="tenantId">The ID of the current tenant scheduling this job.</param>
/// <param name="cancellationToken">The cancellation token.</param>
// ReSharper disable once UnusedParameter.Global
[AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Fail)]
public async Task ExecuteAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken)
{
var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null;
using var scope = tenantAccessor.PushContext(tenant);
var client = await workflowRuntime.CreateClientAsync(cancellationToken);
var createAndRunRequest = new CreateAndRunWorkflowInstanceRequest
{
WorkflowDefinitionHandle = request.WorkflowDefinitionHandle,
TriggerActivityId = request.TriggerActivityId,
CorrelationId = request.CorrelationId,
Input = request.Input,
Properties = request.Properties,
ParentId = request.ParentId
};
await client.CreateAndRunInstanceAsync(createAndRunRequest, cancellationToken);
}
[Obsolete("Use the other overload.")]
[UsedImplicitly]
public async Task ExecuteAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, CancellationToken cancellationToken)
{
var client = await workflowRuntime.CreateClientAsync(cancellationToken);
var createAndRunRequest = new CreateAndRunWorkflowInstanceRequest
{
WorkflowDefinitionHandle = request.WorkflowDefinitionHandle,
TriggerActivityId = request.TriggerActivityId,
CorrelationId = request.CorrelationId,
Input = request.Input,
Properties = request.Properties,
ParentId = request.ParentId
};
await client.CreateAndRunInstanceAsync(createAndRunRequest, cancellationToken);
}
}
@@ -0,0 +1,50 @@
using Elsa.Common.Multitenancy;
using Elsa.Hangfire.Jobs;
using Elsa.Hangfire.States;
using Elsa.Workflows.Runtime;
using Hangfire;
using Hangfire.States;
namespace Elsa.Hangfire.Services;
/// <summary>
/// Invokes activities from a background worker within the context of its workflow instance using Hangfire.
/// </summary>
public class HangfireBackgroundActivityScheduler(IBackgroundJobClient backgroundJobClient, ITenantAccessor tenantAccessor) : IBackgroundActivityScheduler
{
public Task<string> CreateAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
{
var tenantId = tenantAccessor.Tenant?.Id;
var jobId = backgroundJobClient.Create<ExecuteBackgroundActivityJob>(x => x.ExecuteAsync(scheduledBackgroundActivity, tenantId, CancellationToken.None), new PendingState());
return Task.FromResult(jobId);
}
public Task ScheduleAsync(string jobId, CancellationToken cancellationToken = default)
{
using var connection = JobStorage.Current.GetConnection();
var jobData = connection.GetJobData(jobId);
if (jobData != null) backgroundJobClient.ChangeState(jobId, new EnqueuedState());
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<string> ScheduleAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
{
var tenantId = tenantAccessor.Tenant?.Id;
var jobId = backgroundJobClient.Enqueue<ExecuteBackgroundActivityJob>(x => x.ExecuteAsync(scheduledBackgroundActivity, tenantId, CancellationToken.None));
return Task.FromResult(jobId);
}
public Task UnscheduledAsync(string jobId, CancellationToken cancellationToken = default)
{
backgroundJobClient.Delete(jobId);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task CancelAsync(string jobId, CancellationToken cancellationToken = default)
{
backgroundJobClient.Delete(jobId);
return Task.CompletedTask;
}
}
@@ -0,0 +1,104 @@
using Elsa.Common.Multitenancy;
using Elsa.Hangfire.Extensions;
using Elsa.Hangfire.Jobs;
using Elsa.Scheduling;
using Hangfire;
using Hangfire.Storage;
namespace Elsa.Hangfire.Services;
/// <summary>
/// An implementation of <see cref="IWorkflowScheduler"/> that uses Hangfire.
/// </summary>
public class HangfireWorkflowScheduler(
IBackgroundJobClient backgroundJobClient,
IRecurringJobManager recurringJobManager,
ITenantAccessor tenantAccessor,
JobStorage jobStorage) : IWorkflowScheduler
{
/// <inheritdoc />
public ValueTask ScheduleAtAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default)
{
var tenantId = tenantAccessor.Tenant?.Id;
backgroundJobClient.Schedule<RunWorkflowJob>(job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), at);
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public ValueTask ScheduleAtAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default)
{
var tenantId = tenantAccessor.Tenant?.Id;
backgroundJobClient.Schedule<ResumeWorkflowJob>(job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), at);
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public async ValueTask ScheduleRecurringAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, DateTimeOffset startAt, TimeSpan interval, CancellationToken cancellationToken = default)
{
await ScheduleCronAsync(taskName, request, interval.ToCronExpression(), cancellationToken);
}
/// <inheritdoc />
public async ValueTask ScheduleRecurringAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, DateTimeOffset startAt, TimeSpan interval, CancellationToken cancellationToken = default)
{
await ScheduleCronAsync(taskName, request, interval.ToCronExpression(), cancellationToken);
}
/// <inheritdoc />
public ValueTask ScheduleCronAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default)
{
var tenantId = tenantAccessor.Tenant?.Id;
recurringJobManager.AddOrUpdate<RunWorkflowJob>(taskName, job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), cronExpression);
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public ValueTask ScheduleCronAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default)
{
var tenantId = tenantAccessor.Tenant?.Id;
recurringJobManager.AddOrUpdate<ResumeWorkflowJob>(taskName, job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), cronExpression);
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public ValueTask UnscheduleAsync(string taskName, CancellationToken cancellationToken = default)
{
DeleteJobByTaskName(taskName);
return ValueTask.CompletedTask;
}
private void DeleteJobByTaskName(string taskName)
{
var scheduledJobIds = GetScheduledJobIds(taskName);
foreach (var jobId in scheduledJobIds) backgroundJobClient.Delete(jobId);
var queuedJobsIds = GetQueuedJobIds(taskName);
foreach (var jobId in queuedJobsIds) backgroundJobClient.Delete(jobId);
var recurringJobIds = GetRecurringJobIds<RunWorkflowJob>(taskName);
foreach (var jobId in recurringJobIds) recurringJobManager.RemoveIfExists(jobId);
}
private IEnumerable<string> GetScheduledJobIds(string taskName)
{
return jobStorage.EnumerateScheduledJobs(taskName)
.Select(x => x.Key)
.Distinct()
.ToList();
}
private IEnumerable<string> GetQueuedJobIds(string taskName)
{
return jobStorage.EnumerateQueuedJobs("default", taskName)
.Select(x => x.Key)
.Distinct()
.ToList();
}
private IEnumerable<string> GetRecurringJobIds<TJob>(string taskName)
{
using var connection = jobStorage.GetConnection();
var jobs = connection.GetRecurringJobs().Where(x => x.Job.Type == typeof(TJob) && (string)x.Job.Args[0] == taskName);
return jobs.Select(x => x.Id).Distinct().ToList();
}
}
@@ -0,0 +1,18 @@
using Hangfire.States;
namespace Elsa.Hangfire.States;
public class PendingState : IState
{
public static readonly string StateName = "Pending";
public Dictionary<string, string> SerializeData()
{
return new();
}
public string Name => StateName;
public string Reason => "Pending";
public bool IsFinal => false;
public bool IgnoreJobLoadException => true;
}