Update Hangfire integration (#3898)
* Add Hangfire support for workflow scheduler * Fix scheduled job cleanup * Fix duplicate handler invocation * Default scheduler fixes
This commit is contained in:
parent
56b284c230
commit
83449f0508
7
Elsa.sln
7
Elsa.sln
|
|
@ -170,6 +170,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Api.Client", "src\clie
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.QuartzIntegration", "src\samples\aspnet\Elsa.Samples.QuartzIntegration\Elsa.Samples.QuartzIntegration.csproj", "{B0312D9E-FA30-43E9-B666-40A8782D6E1C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.HangfireIntegration", "src\samples\aspnet\Elsa.Samples.HangfireIntegration\Elsa.Samples.HangfireIntegration.csproj", "{D2614FC7-102F-4F78-BB06-7C87304A10BA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -420,6 +422,10 @@ Global
|
|||
{B0312D9E-FA30-43E9-B666-40A8782D6E1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B0312D9E-FA30-43E9-B666-40A8782D6E1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B0312D9E-FA30-43E9-B666-40A8782D6E1C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D2614FC7-102F-4F78-BB06-7C87304A10BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D2614FC7-102F-4F78-BB06-7C87304A10BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D2614FC7-102F-4F78-BB06-7C87304A10BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D2614FC7-102F-4F78-BB06-7C87304A10BA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
|
||||
|
|
@ -494,5 +500,6 @@ Global
|
|||
{89608AA5-5ADE-4832-AC7B-871C4AE64210} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
|
||||
{1FCB2200-28B8-4703-8E89-73241AAED047} = {89608AA5-5ADE-4832-AC7B-871C4AE64210}
|
||||
{B0312D9E-FA30-43E9-B666-40A8782D6E1C} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
|
||||
{D2614FC7-102F-4F78-BB06-7C87304A10BA} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {Moment} from "moment";
|
|||
export interface NotificationType {
|
||||
id?: number | any;
|
||||
title: string;
|
||||
text: string | JSX.Element;
|
||||
text: string | any;
|
||||
type?: NotificationDisplayType;
|
||||
timestamp?: Moment;
|
||||
showToast?: boolean;
|
||||
|
|
|
|||
|
|
@ -14,10 +14,12 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Hangfire" Version="1.7.33" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
|
||||
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
|
||||
<ProjectReference Include="..\Elsa.Scheduling\Elsa.Scheduling.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
32
src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs
Normal file
32
src/modules/Elsa.Hangfire/Extensions/JobStorageExtensions.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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<TJob>(this JobStorage storage, string name)
|
||||
{
|
||||
var api = storage.GetMonitoringApi();
|
||||
var skip = 0;
|
||||
const int take = 100;
|
||||
JobList<ScheduledJobDto> jobList;
|
||||
|
||||
do
|
||||
{
|
||||
jobList = api.ScheduledJobs(skip, take);
|
||||
|
||||
var jobs = jobList.FindAll(x => x.Value.Job.Type == typeof(TJob));
|
||||
foreach (var job in jobs.Where(x => (string)x.Value.Job.Args[0] == name))
|
||||
yield return job;
|
||||
|
||||
skip += take;
|
||||
} while (jobList.Count == take);
|
||||
}
|
||||
}
|
||||
61
src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs
Normal file
61
src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
using Elsa.Features.Services;
|
||||
using Elsa.Hangfire.Features;
|
||||
using Elsa.Hangfire.Services;
|
||||
using Elsa.Scheduling.Contracts;
|
||||
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>
|
||||
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>
|
||||
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;
|
||||
}
|
||||
}
|
||||
23
src/modules/Elsa.Hangfire/Extensions/TimeSpanExtensions.cs
Normal file
23
src/modules/Elsa.Hangfire/Extensions/TimeSpanExtensions.cs
Normal file
|
|
@ -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,38 @@
|
|||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Hangfire.Services;
|
||||
using Elsa.Scheduling.Contracts;
|
||||
using Elsa.Scheduling.Features;
|
||||
using Elsa.Workflows.Runtime.Contracts;
|
||||
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.BackgroundActivityInvoker = sp => sp.GetRequiredService<HangfireBackgroundActivityScheduler>();
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.AddSingleton<HangfireBackgroundActivityScheduler>();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,13 +2,12 @@ using Elsa.Features.Abstractions;
|
|||
using Elsa.Features.Services;
|
||||
using Hangfire;
|
||||
using Hangfire.MemoryStorage;
|
||||
using Hangfire.SqlServer;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Elsa.Hangfire.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up Hangfire.
|
||||
/// Sets up Hangfire. If you're setting up Hangfire yourself, then you should not enable this feature.
|
||||
/// </summary>
|
||||
public class HangfireFeature : FeatureBase
|
||||
{
|
||||
|
|
@ -18,47 +17,39 @@ public class HangfireFeature : FeatureBase
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether to use SQL Server storage.
|
||||
/// A delegate that configures Hangfire.
|
||||
/// </summary>
|
||||
public bool UseSqlServerStorage { get; set; }
|
||||
public Action<IServiceProvider, IGlobalConfiguration> ConfigureHangfire { get; set; } = (_, cfg) => cfg.UseMemoryStorage();
|
||||
|
||||
/// <summary>
|
||||
/// A delegate that configures Hangfire's background job server options.
|
||||
/// </summary>
|
||||
public Action<IServiceProvider, BackgroundJobServerOptions> ConfigureBackgroundServerOptions { get; set; } = (_, _) => { };
|
||||
|
||||
/// <summary>
|
||||
/// The SQL Server storage options.
|
||||
/// A delegate that creates a job storage instance.
|
||||
/// </summary>
|
||||
public SqlServerStorageOptions? SqlServerStorageOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The SQL Server connection string.
|
||||
/// </summary>
|
||||
public string? SqlServerConnectionString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Hangfire background server options.
|
||||
/// </summary>
|
||||
public Action<BackgroundJobServerOptions>? ConfigureBackgroundServerOptions { get; set; }
|
||||
public Func<JobStorage> CreateJobStorage { get; set; } = () => new MemoryStorage();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
public override void Apply()
|
||||
{
|
||||
Services.AddHangfire(configuration =>
|
||||
Action<IServiceProvider, IGlobalConfiguration> configAction = (sp, cfg) =>
|
||||
{
|
||||
configuration.UseSimpleAssemblyNameTypeSerializer();
|
||||
configuration.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects);
|
||||
|
||||
if (UseSqlServerStorage)
|
||||
{
|
||||
var storageOptions = SqlServerStorageOptions ?? new SqlServerStorageOptions();
|
||||
configuration.UseSqlServerStorage(SqlServerConnectionString, storageOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
configuration.UseMemoryStorage();
|
||||
}
|
||||
});
|
||||
|
||||
if (UseSqlServerStorage)
|
||||
Services.AddHangfireServer((_, options) => ConfigureBackgroundServerOptions?.Invoke(options), new SqlServerStorage(SqlServerConnectionString));
|
||||
else
|
||||
Services.AddHangfireServer(options => { ConfigureBackgroundServerOptions?.Invoke(options); });
|
||||
cfg.UseSimpleAssemblyNameTypeSerializer();
|
||||
cfg.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects);
|
||||
};
|
||||
|
||||
Action<IServiceProvider, BackgroundJobServerOptions> serverOptionsAction = (sp, options) =>
|
||||
{
|
||||
options.WorkerCount = 1;
|
||||
options.SchedulePollingInterval = TimeSpan.FromSeconds(1);
|
||||
};
|
||||
|
||||
configAction += ConfigureHangfire;
|
||||
serverOptionsAction += ConfigureBackgroundServerOptions;
|
||||
|
||||
Services.AddHangfire(configAction);
|
||||
Services.AddHangfireServer(serverOptionsAction, CreateJobStorage());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Hangfire.Services;
|
||||
using Elsa.Scheduling.Contracts;
|
||||
using Elsa.Scheduling.Features;
|
||||
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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
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))]
|
||||
public class HangfireSqlServerStorageFeature : FeatureBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public HangfireSqlServerStorageFeature(IModule module) : base(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; } = default!;
|
||||
|
||||
/// <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);
|
||||
};
|
||||
|
||||
hangfireFeature.CreateJobStorage = () => new SqlServerStorage(NameOrConnectionString, storageOptions);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Hangfire;
|
||||
using Hangfire.SqlServer;
|
||||
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))]
|
||||
public class HangfireSqliteStorageFeature : FeatureBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public HangfireSqliteStorageFeature(IModule module) : base(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; } = default!;
|
||||
|
||||
/// <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);
|
||||
};
|
||||
|
||||
hangfireFeature.CreateJobStorage = () => new SQLiteStorage(NameOrConnectionString, storageOptions);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Models;
|
||||
|
||||
namespace Elsa.Hangfire.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// A job that executes a background activity.
|
||||
/// </summary>
|
||||
public class ExecuteBackgroundActivityJob
|
||||
{
|
||||
private readonly IBackgroundActivityInvoker _backgroundActivityInvoker;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExecuteBackgroundActivityJob"/> class.
|
||||
/// </summary>
|
||||
/// <param name="backgroundActivityInvoker"></param>
|
||||
public ExecuteBackgroundActivityJob(IBackgroundActivityInvoker backgroundActivityInvoker)
|
||||
{
|
||||
_backgroundActivityInvoker = backgroundActivityInvoker;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the job.
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken);
|
||||
}
|
||||
}
|
||||
28
src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs
Normal file
28
src/modules/Elsa.Hangfire/Jobs/ResumeWorkflowJob.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Models.Requests;
|
||||
|
||||
namespace Elsa.Hangfire.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// A job that resumes a workflow.
|
||||
/// </summary>
|
||||
public class ResumeWorkflowJob
|
||||
{
|
||||
private readonly IWorkflowDispatcher _workflowDispatcher;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ResumeWorkflowJob"/> class.
|
||||
/// </summary>
|
||||
public ResumeWorkflowJob(IWorkflowDispatcher workflowDispatcher)
|
||||
{
|
||||
_workflowDispatcher = workflowDispatcher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the job.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the job.</param>
|
||||
/// <param name="request">The workflow request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public async Task ExecuteAsync(string name, DispatchWorkflowInstanceRequest request, CancellationToken cancellationToken) => await _workflowDispatcher.DispatchAsync(request, cancellationToken);
|
||||
}
|
||||
28
src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs
Normal file
28
src/modules/Elsa.Hangfire/Jobs/RunWorkflowJob.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Models.Requests;
|
||||
|
||||
namespace Elsa.Hangfire.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// A job that resumes a workflow.
|
||||
/// </summary>
|
||||
public class RunWorkflowJob
|
||||
{
|
||||
private readonly IWorkflowDispatcher _workflowDispatcher;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RunWorkflowJob"/> class.
|
||||
/// </summary>
|
||||
public RunWorkflowJob(IWorkflowDispatcher workflowDispatcher)
|
||||
{
|
||||
_workflowDispatcher = workflowDispatcher;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the job.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the job.</param>
|
||||
/// <param name="request">The workflow request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public async Task ExecuteAsync(string name, DispatchWorkflowDefinitionRequest request, CancellationToken cancellationToken) => await _workflowDispatcher.DispatchAsync(request, cancellationToken);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using Elsa.Hangfire.Jobs;
|
||||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Models;
|
||||
using Hangfire;
|
||||
|
||||
namespace Elsa.Hangfire.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes activities from a background worker within the context of its workflow instance using Hangfire.
|
||||
/// </summary>
|
||||
public class HangfireBackgroundActivityScheduler : IBackgroundActivityScheduler
|
||||
{
|
||||
private readonly IBackgroundJobClient _backgroundJobClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HangfireBackgroundActivityScheduler"/> class.
|
||||
/// </summary>
|
||||
public HangfireBackgroundActivityScheduler(IBackgroundJobClient backgroundJobClient)
|
||||
{
|
||||
_backgroundJobClient = backgroundJobClient;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> ScheduleAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var jobId = _backgroundJobClient.Enqueue<ExecuteBackgroundActivityJob>(x => x.ExecuteAsync(scheduledBackgroundActivity, CancellationToken.None));
|
||||
return Task.FromResult(jobId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
using Elsa.Hangfire.Extensions;
|
||||
using Elsa.Hangfire.Jobs;
|
||||
using Elsa.Scheduling.Contracts;
|
||||
using Elsa.Workflows.Runtime.Models.Requests;
|
||||
using Hangfire;
|
||||
using Hangfire.Storage;
|
||||
|
||||
namespace Elsa.Hangfire.Services;
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of <see cref="Scheduling.Contracts.IWorkflowScheduler"/> that uses Hangfire.
|
||||
/// </summary>
|
||||
public class HangfireWorkflowScheduler : IWorkflowScheduler
|
||||
{
|
||||
private readonly IBackgroundJobClient _backgroundJobClient;
|
||||
private readonly IRecurringJobManager _recurringJobManager;
|
||||
private readonly JobStorage _jobStorage;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HangfireWorkflowScheduler"/> class.
|
||||
/// </summary>
|
||||
public HangfireWorkflowScheduler(IBackgroundJobClient backgroundJobClient, IRecurringJobManager recurringJobManager, JobStorage jobStorage)
|
||||
{
|
||||
_backgroundJobClient = backgroundJobClient;
|
||||
_recurringJobManager = recurringJobManager;
|
||||
_jobStorage = jobStorage;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask ScheduleAtAsync(string taskName, DispatchWorkflowDefinitionRequest request, DateTimeOffset at, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_backgroundJobClient.Schedule<RunWorkflowJob>(job => job.ExecuteAsync(taskName, request, CancellationToken.None), at);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask ScheduleAtAsync(string taskName, DispatchWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_backgroundJobClient.Schedule<ResumeWorkflowJob>(job => job.ExecuteAsync(taskName, request, CancellationToken.None), at);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ScheduleRecurringAsync(string taskName, DispatchWorkflowDefinitionRequest request, DateTimeOffset startAt, TimeSpan interval, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ScheduleCronAsync(taskName, request, interval.ToCronExpression(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ScheduleRecurringAsync(string taskName, DispatchWorkflowInstanceRequest request, DateTimeOffset startAt, TimeSpan interval, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ScheduleCronAsync(taskName, request, interval.ToCronExpression(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask ScheduleCronAsync(string taskName, DispatchWorkflowDefinitionRequest request, string cronExpression, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_recurringJobManager.AddOrUpdate<RunWorkflowJob>(taskName, job => job.ExecuteAsync(taskName, request, CancellationToken.None), cronExpression);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask ScheduleCronAsync(string taskName, DispatchWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_recurringJobManager.AddOrUpdate<ResumeWorkflowJob>(taskName, job => job.ExecuteAsync(taskName, request, 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<RunWorkflowJob>(taskName);
|
||||
foreach (var jobId in scheduledJobIds) _backgroundJobClient.Delete(jobId);
|
||||
|
||||
var recurringJobIds = GetRecurringJobIds<RunWorkflowJob>(taskName);
|
||||
foreach (var jobId in recurringJobIds) _recurringJobManager.RemoveIfExists(jobId);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetScheduledJobIds<TJob>(string taskName)
|
||||
{
|
||||
return _jobStorage.EnumerateScheduledJobs<TJob>(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();
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ public class DispatchWorkflowRequestConsumer :
|
|||
var message = context.Message;
|
||||
var options = new StartWorkflowRuntimeOptions(message.CorrelationId, message.Input, message.VersionOptions, InstanceId: message.InstanceId);
|
||||
|
||||
await _workflowRuntime.StartWorkflowAsync(message.DefinitionId, options, context.CancellationToken);
|
||||
await _workflowRuntime.TryStartWorkflowAsync(message.DefinitionId, options, context.CancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public class NotificationHandlerInvokerMiddleware : INotificationMiddleware
|
|||
var notification = context.Notification;
|
||||
var notificationType = notification.GetType();
|
||||
var handlerType = typeof(INotificationHandler<>).MakeGenericType(notificationType);
|
||||
var handlers = _notificationHandlers.Where(x => handlerType.IsInstanceOfType(x)).ToArray();
|
||||
var handlers = _notificationHandlers.Where(x => handlerType.IsInstanceOfType(x)).DistinctBy(x => x.GetType()).ToArray();
|
||||
var handleMethod = handlerType.GetMethod("HandleAsync")!;
|
||||
var cancellationToken = context.CancellationToken;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
|
|||
private readonly ITriggerStore _triggerStore;
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
private readonly IBookmarkHasher _hasher;
|
||||
private readonly IWorkflowDefinitionService _workflowDefinitionService;
|
||||
private readonly IWorkflowInstanceFactory _workflowInstanceFactory;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -33,6 +34,7 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
|
|||
ITriggerStore triggerStore,
|
||||
IIdentityGenerator identityGenerator,
|
||||
IBookmarkHasher hasher,
|
||||
IWorkflowDefinitionService workflowDefinitionService,
|
||||
IWorkflowInstanceFactory workflowInstanceFactory)
|
||||
{
|
||||
_cluster = cluster;
|
||||
|
|
@ -40,6 +42,7 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
|
|||
_triggerStore = triggerStore;
|
||||
_identityGenerator = identityGenerator;
|
||||
_hasher = hasher;
|
||||
_workflowDefinitionService = workflowDefinitionService;
|
||||
_workflowInstanceFactory = workflowInstanceFactory;
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +70,18 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
|
|||
return new CanStartWorkflowResult(workflowInstanceId, response!.CanStart);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WorkflowExecutionResult?> TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Load the workflow definition.
|
||||
var workflowDefinition = await _workflowDefinitionService.FindAsync(definitionId, options.VersionOptions, cancellationToken);
|
||||
|
||||
if (workflowDefinition == null)
|
||||
return null;
|
||||
|
||||
return await StartWorkflowAsync(definitionId, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WorkflowExecutionResult> StartWorkflowAsync(string definitionId, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
@ -177,7 +192,7 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime
|
|||
|
||||
var collectedResumableWorkflow = (match as ResumableWorkflowMatch)!;
|
||||
var runtimeOptions = new ResumeWorkflowRuntimeOptions(collectedResumableWorkflow.CorrelationId, Input: input);
|
||||
|
||||
|
||||
var resumeResult = await ResumeWorkflowAsync(
|
||||
match.WorkflowInstanceId,
|
||||
runtimeOptions with { BookmarkId = collectedResumableWorkflow.BookmarkId },
|
||||
|
|
|
|||
|
|
@ -33,4 +33,13 @@ public static class JobDataMapExtensions
|
|||
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a dictionary from the map.
|
||||
/// </summary>
|
||||
public static IDictionary<string, object>? GetDictionary(this JobDataMap map, string key)
|
||||
{
|
||||
var json = (string?)map.Get(key);
|
||||
return json == null ? null : JsonSerializer.Deserialize<Dictionary<string, object>>(json);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ using Elsa.Scheduling.Features;
|
|||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="SchedulingFeature"/>.
|
||||
/// Provides extension methods for the <see cref="SchedulingFeature"/>.
|
||||
/// </summary>
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Models.Requests;
|
||||
using Quartz;
|
||||
|
|
@ -35,7 +36,8 @@ public class ResumeWorkflowJob : IJob
|
|||
ActivityId = (string?)map.Get(nameof(DispatchWorkflowInstanceRequest.ActivityId)),
|
||||
ActivityNodeId = (string?)map.Get(nameof(DispatchWorkflowInstanceRequest.ActivityNodeId)),
|
||||
ActivityInstanceId = (string?)map.Get(nameof(DispatchWorkflowInstanceRequest.ActivityInstanceId)),
|
||||
CorrelationId = (string?)map.Get(nameof(DispatchWorkflowInstanceRequest.CorrelationId))
|
||||
CorrelationId = (string?)map.Get(nameof(DispatchWorkflowInstanceRequest.CorrelationId)),
|
||||
Input = map.GetDictionary(nameof(DispatchWorkflowInstanceRequest.Input))
|
||||
};
|
||||
await _workflowDispatcher.DispatchAsync(request, context.CancellationToken);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Common.Models;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Models.Requests;
|
||||
using Quartz;
|
||||
|
|
@ -40,7 +41,8 @@ public class RunWorkflowJob : IJob
|
|||
VersionOptions = VersionOptions.FromString((string)map.Get(nameof(DispatchWorkflowDefinitionRequest.VersionOptions))),
|
||||
TriggerActivityId = (string?)map.Get(nameof(DispatchWorkflowDefinitionRequest.TriggerActivityId)),
|
||||
InstanceId = (string?)map.Get(nameof(DispatchWorkflowDefinitionRequest.InstanceId)),
|
||||
CorrelationId = (string?)map.Get(nameof(DispatchWorkflowDefinitionRequest.CorrelationId))
|
||||
CorrelationId = (string?)map.Get(nameof(DispatchWorkflowDefinitionRequest.CorrelationId)),
|
||||
Input = map.GetDictionary(nameof(DispatchWorkflowDefinitionRequest.Input))
|
||||
};
|
||||
await _workflowDispatcher.DispatchAsync(request, context.CancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ public class SchedulingFeature : FeatureBase
|
|||
.AddSingleton<ITriggerScheduler, DefaultTriggerScheduler>()
|
||||
.AddSingleton<IBookmarkScheduler, DefaultBookmarkScheduler>()
|
||||
.AddSingleton<IScheduler, LocalScheduler>()
|
||||
.AddSingleton<DefaultWorkflowScheduler>()
|
||||
.AddSingleton(WorkflowScheduler)
|
||||
.AddHandlersFrom<ScheduleWorkflows>();
|
||||
|
||||
|
|
|
|||
|
|
@ -48,28 +48,36 @@ public class ScheduledRecurringTask : IScheduledTask
|
|||
|
||||
private void Schedule()
|
||||
{
|
||||
var startAt = _startAt;
|
||||
var adjusted = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var now = _systemClock.UtcNow;
|
||||
var delay = _startAt - now;
|
||||
var delay = startAt - now;
|
||||
|
||||
if (delay.Milliseconds <= 0)
|
||||
if (!adjusted && delay.Milliseconds <= 0)
|
||||
{
|
||||
adjusted = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
SetupTimer(delay, now);
|
||||
SetupTimer(delay);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupTimer(TimeSpan delay, DateTimeOffset now)
|
||||
private void SetupTimer(TimeSpan delay)
|
||||
{
|
||||
if(delay < TimeSpan.Zero) delay = TimeSpan.FromSeconds(1);
|
||||
|
||||
_timer = new Timer(delay.TotalMilliseconds) { Enabled = true };
|
||||
|
||||
_timer.Elapsed += async (_, _) =>
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
_startAt = now + _interval;
|
||||
_startAt = _systemClock.UtcNow + _interval;
|
||||
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
if (!cancellationToken.IsCancellationRequested) await _commandSender.SendAsync(new RunScheduledTask(_task), cancellationToken);
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public class DefaultTriggerScheduler : ITriggerScheduler
|
|||
var timerTriggers = triggerList.Filter<Activities.Timer>().ToList();
|
||||
|
||||
// Select all StartAt triggers.
|
||||
var startAtTriggers = triggerList.Filter<Activities.Timer>().ToList();
|
||||
var startAtTriggers = triggerList.Filter<StartAt>().ToList();
|
||||
|
||||
// Concatenate the filtered triggers.
|
||||
var filteredTriggers = timerTriggers.Concat(startAtTriggers).ToList();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ public interface IWorkflowRuntime
|
|||
TriggerWorkflowsRuntimeOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to start a workflow and returns the result if successful.
|
||||
/// </summary>
|
||||
Task<WorkflowExecutionResult?> TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resumes an existing workflow instance.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ internal class DispatchWorkflowRequestHandler :
|
|||
{
|
||||
var options = new StartWorkflowRuntimeOptions(command.CorrelationId, command.Input, command.VersionOptions, InstanceId: command.InstanceId, TriggerActivityId: command.TriggerActivityId);
|
||||
|
||||
await _workflowRuntime.StartWorkflowAsync(command.DefinitionId, options, cancellationToken);
|
||||
await _workflowRuntime.TryStartWorkflowAsync(command.DefinitionId, options, cancellationToken);
|
||||
|
||||
return Unit.Instance;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ using Elsa.Common.Models;
|
|||
using Elsa.Workflows.Core.Contracts;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.State;
|
||||
using Elsa.Workflows.Management.Entities;
|
||||
using Elsa.Workflows.Runtime.Contracts;
|
||||
using Elsa.Workflows.Runtime.Models;
|
||||
using Medallion.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Workflows.Runtime.Services;
|
||||
|
||||
|
|
@ -21,6 +23,7 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
|
|||
private readonly IBookmarkHasher _hasher;
|
||||
private readonly IDistributedLockProvider _distributedLockProvider;
|
||||
private readonly IWorkflowInstanceFactory _workflowInstanceFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
|
|
@ -33,7 +36,8 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
|
|||
IBookmarkStore bookmarkStore,
|
||||
IBookmarkHasher hasher,
|
||||
IDistributedLockProvider distributedLockProvider,
|
||||
IWorkflowInstanceFactory workflowInstanceFactory)
|
||||
IWorkflowInstanceFactory workflowInstanceFactory,
|
||||
ILogger<DefaultWorkflowRuntime> logger)
|
||||
{
|
||||
_workflowHostFactory = workflowHostFactory;
|
||||
_workflowDefinitionService = workflowDefinitionService;
|
||||
|
|
@ -43,6 +47,7 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
|
|||
_hasher = hasher;
|
||||
_distributedLockProvider = distributedLockProvider;
|
||||
_workflowInstanceFactory = workflowInstanceFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -59,16 +64,19 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
|
|||
/// <inheritdoc />
|
||||
public async Task<WorkflowExecutionResult> StartWorkflowAsync(string definitionId, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var input = options.Input;
|
||||
var correlationId = options.CorrelationId;
|
||||
var workflowHost = await CreateWorkflowHostAsync(definitionId, options, cancellationToken);
|
||||
var startWorkflowOptions = new StartWorkflowHostOptions(options.InstanceId, correlationId, input, options.TriggerActivityId);
|
||||
await workflowHost.StartWorkflowAsync(startWorkflowOptions, cancellationToken);
|
||||
var workflowState = workflowHost.WorkflowState;
|
||||
return await StartWorkflowAsync(workflowHost, options, cancellationToken);
|
||||
}
|
||||
|
||||
await SaveWorkflowStateAsync(workflowState, cancellationToken);
|
||||
/// <inheritdoc />
|
||||
public async Task<WorkflowExecutionResult?> TryStartWorkflowAsync(string definitionId, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var workflowDefinition = await FindWorkflowDefinitionAsync(definitionId, options.VersionOptions, cancellationToken);
|
||||
|
||||
return new WorkflowExecutionResult(workflowState.Id, workflowState.Bookmarks);
|
||||
if (workflowDefinition == null)
|
||||
return null;
|
||||
|
||||
return await StartWorkflowAsync(workflowDefinition, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -125,7 +133,10 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
|
|||
cancellationToken);
|
||||
|
||||
if (workflowDefinition == null)
|
||||
throw new Exception("Specified workflow definition and version does not exist");
|
||||
{
|
||||
_logger.LogInformation("The workflow definition {DefinitionId} version {Version} was not found", definitionId, version);
|
||||
return new ResumeWorkflowResult(Array.Empty<Bookmark>());
|
||||
}
|
||||
|
||||
var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(workflowDefinition, cancellationToken);
|
||||
var workflowHost = await _workflowHostFactory.CreateAsync(workflow, workflowState, cancellationToken);
|
||||
|
|
@ -215,14 +226,43 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime
|
|||
/// <inheritdoc />
|
||||
public async Task<int> CountRunningWorkflowsAsync(CountRunningWorkflowsArgs args, CancellationToken cancellationToken = default) => await _workflowStateStore.CountAsync(args, cancellationToken);
|
||||
|
||||
private async Task<WorkflowExecutionResult> StartWorkflowAsync(WorkflowDefinition workflowDefinition, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var workflowHost = await CreateWorkflowHostAsync(workflowDefinition, cancellationToken);
|
||||
return await StartWorkflowAsync(workflowHost, options, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<WorkflowExecutionResult> StartWorkflowAsync(IWorkflowHost workflowHost, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var input = options.Input;
|
||||
var correlationId = options.CorrelationId;
|
||||
var startWorkflowOptions = new StartWorkflowHostOptions(options.InstanceId, correlationId, input, options.TriggerActivityId);
|
||||
await workflowHost.StartWorkflowAsync(startWorkflowOptions, cancellationToken);
|
||||
var workflowState = workflowHost.WorkflowState;
|
||||
|
||||
await SaveWorkflowStateAsync(workflowState, cancellationToken);
|
||||
|
||||
return new WorkflowExecutionResult(workflowState.Id, workflowState.Bookmarks);
|
||||
}
|
||||
|
||||
private async Task<WorkflowDefinition?> FindWorkflowDefinitionAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _workflowDefinitionService.FindAsync(definitionId, versionOptions, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<IWorkflowHost> CreateWorkflowHostAsync(string definitionId, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
var versionOptions = options.VersionOptions;
|
||||
var workflowDefinition = await _workflowDefinitionService.FindAsync(definitionId, versionOptions, cancellationToken);
|
||||
var workflowDefinition = await FindWorkflowDefinitionAsync(definitionId, versionOptions, cancellationToken);
|
||||
|
||||
if (workflowDefinition == null)
|
||||
throw new Exception("Specified workflow definition and version does not exist");
|
||||
|
||||
|
||||
return await CreateWorkflowHostAsync(workflowDefinition, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<IWorkflowHost> CreateWorkflowHostAsync(WorkflowDefinition workflowDefinition, CancellationToken cancellationToken)
|
||||
{
|
||||
var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(workflowDefinition, cancellationToken);
|
||||
return await _workflowHostFactory.CreateAsync(workflow, cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\bundles\Elsa\Elsa.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Hangfire\Elsa.Hangfire.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Http\Elsa.Http.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Identity\Elsa.Identity.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Scheduling\Elsa.Scheduling.csproj" />
|
||||
<ProjectReference Include="..\..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
using Elsa.EntityFrameworkCore.Extensions;
|
||||
using Elsa.EntityFrameworkCore.Modules.Management;
|
||||
using Elsa.EntityFrameworkCore.Modules.Runtime;
|
||||
using Elsa.Extensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var configuration = builder.Configuration;
|
||||
var identitySection = configuration.GetSection("Identity");
|
||||
var identityTokenSection = identitySection.GetSection("Tokens");
|
||||
|
||||
// Add Elsa to the container.
|
||||
builder.Services.AddElsa(elsa =>
|
||||
{
|
||||
// Configure management feature to use EF Core.
|
||||
elsa.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite()));
|
||||
|
||||
elsa.UseWorkflowRuntime(runtime =>
|
||||
{
|
||||
runtime.UseDefaultRuntime(dr => dr.UseEntityFrameworkCore(ef => ef.UseSqlite()));
|
||||
|
||||
// Use Hangfire to schedule background activities.
|
||||
runtime.UseHangfireBackgroundActivityScheduler();
|
||||
|
||||
// Capture execution log records.
|
||||
runtime.UseExecutionLogRecords(e => e.UseEntityFrameworkCore(ef => ef.UseSqlite()));
|
||||
|
||||
// Capture workflow state.
|
||||
runtime.UseAsyncWorkflowStateExporter();
|
||||
});
|
||||
|
||||
// Expose API endpoints.
|
||||
elsa.UseWorkflowsApi();
|
||||
|
||||
// Use Hangfire.
|
||||
elsa.UseHangfire(hangfire => hangfire.UseSqliteStorage(sqlite => sqlite.NameOrConnectionString = "elsa.sqlite.db"));
|
||||
|
||||
// Use hangfire for scheduling timer events.
|
||||
elsa.UseScheduling(scheduling => scheduling.UseHangfireScheduler());
|
||||
|
||||
// Configure identity.
|
||||
elsa.UseIdentity(identity =>
|
||||
{
|
||||
identity.IdentityOptions = options => identitySection.Bind(options);
|
||||
identity.TokenOptions = options => identityTokenSection.Bind(options);
|
||||
identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options));
|
||||
identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options));
|
||||
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
|
||||
});
|
||||
|
||||
// Use default authentication (JWT).
|
||||
elsa.UseDefaultAuthentication();
|
||||
});
|
||||
|
||||
// Configure CORS to allow designer app hosted on a different origin to invoke the APIs.
|
||||
builder.Services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()));
|
||||
|
||||
// Build the web app.
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the web app's request pipeline.
|
||||
app.UseHttpsRedirection();
|
||||
app.UseCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseWorkflowsApi();
|
||||
app.UseWorkflows();
|
||||
|
||||
// Run the web app.
|
||||
app.Run();
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:3978",
|
||||
"sslPort": 44367
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5090",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7020;http://localhost:5090",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
## Secrets
|
||||
The following are the secrets stored in hashed form in appsettings.json:
|
||||
|
||||
**API key**: `4E753976726458745954355043687772-e54d5a2c-33a3-4c05-a216-b09569062aed`
|
||||
**Admin user**: `admin`
|
||||
**Admin password**: `password`
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Warning",
|
||||
"Microsoft.Hosting": "Information",
|
||||
"Hangfire": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Identity": {
|
||||
"Tokens": {
|
||||
"SigningKey": "secret-signing-key",
|
||||
"AccessTokenLifetime": "1:00:00:00",
|
||||
"RefreshTokenLifetime": "1:00:10:00"
|
||||
},
|
||||
"Roles": [{
|
||||
"Id": "admin",
|
||||
"Name": "Administrator",
|
||||
"Permissions": ["*"]
|
||||
}],
|
||||
"Users": [
|
||||
{
|
||||
"Id": "a2323f46-42db-4e15-af8b-94238717d817",
|
||||
"Name": "admin",
|
||||
"HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=",
|
||||
"HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=",
|
||||
"Roles": ["admin"]
|
||||
}
|
||||
],
|
||||
"Applications": [{
|
||||
"id": "529572c2df854b13807b8bf23f1784cd",
|
||||
"name": "Postman",
|
||||
"roles": [
|
||||
"admin"
|
||||
],
|
||||
"clientId": "Nu9vrdXtYT5PChwr",
|
||||
"clientSecret": "011pp2C$|j01-qrMZpC9VC0F00XCJq(5",
|
||||
"hashedApiKey": "d0rDld3A+ugKmdctGtMzOLTYjQFkOlUWN+kt0VyW9D0=",
|
||||
"hashedApiKeySalt": "EnutGOyy5MuJWV0fF5jCQiciK7a8PU/DRF+fr6nekSY=",
|
||||
"hashedClientSecret": "ERia2zBcCSWb/9dvB0grQ9yf7fWgFrClNeR8A5RMTzk=",
|
||||
"hashedClientSecretSalt": "z3z8KmzHt+xkAj/zYTXcB8I7y0xAkLm95v4Er/oNqiY="
|
||||
}]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue